@silo-code/sdk 0.28.1 → 0.29.0

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 (183) hide show
  1. package/dist/AddRow.d.ts +21 -0
  2. package/dist/AddRow.d.ts.map +1 -0
  3. package/dist/AddRow.js +24 -0
  4. package/dist/AddRow.js.map +1 -0
  5. package/dist/Badge.d.ts +32 -0
  6. package/dist/Badge.d.ts.map +1 -0
  7. package/dist/Badge.js +32 -0
  8. package/dist/Badge.js.map +1 -0
  9. package/dist/Button.d.ts +29 -0
  10. package/dist/Button.d.ts.map +1 -0
  11. package/dist/Button.js +25 -0
  12. package/dist/Button.js.map +1 -0
  13. package/dist/Callout.d.ts +26 -0
  14. package/dist/Callout.d.ts.map +1 -0
  15. package/dist/Callout.js +26 -0
  16. package/dist/Callout.js.map +1 -0
  17. package/dist/CheckboxRow.d.ts +27 -0
  18. package/dist/CheckboxRow.d.ts.map +1 -0
  19. package/dist/CheckboxRow.js +24 -0
  20. package/dist/CheckboxRow.js.map +1 -0
  21. package/dist/EmptyState.d.ts +40 -0
  22. package/dist/EmptyState.d.ts.map +1 -0
  23. package/dist/EmptyState.js +34 -0
  24. package/dist/EmptyState.js.map +1 -0
  25. package/dist/IconButton.d.ts +35 -0
  26. package/dist/IconButton.d.ts.map +1 -0
  27. package/dist/IconButton.js +31 -0
  28. package/dist/IconButton.js.map +1 -0
  29. package/dist/InlineEdit.d.ts +63 -0
  30. package/dist/InlineEdit.d.ts.map +1 -0
  31. package/dist/InlineEdit.js +121 -0
  32. package/dist/InlineEdit.js.map +1 -0
  33. package/dist/Input.d.ts +26 -0
  34. package/dist/Input.d.ts.map +1 -0
  35. package/dist/Input.js +28 -0
  36. package/dist/Input.js.map +1 -0
  37. package/dist/List.d.ts +74 -0
  38. package/dist/List.d.ts.map +1 -0
  39. package/dist/List.js +104 -0
  40. package/dist/List.js.map +1 -0
  41. package/dist/ModalActions.d.ts +40 -0
  42. package/dist/ModalActions.d.ts.map +1 -0
  43. package/dist/ModalActions.js +36 -0
  44. package/dist/ModalActions.js.map +1 -0
  45. package/dist/RadioGroup.d.ts +55 -0
  46. package/dist/RadioGroup.d.ts.map +1 -0
  47. package/dist/RadioGroup.js +60 -0
  48. package/dist/RadioGroup.js.map +1 -0
  49. package/dist/SearchInput.d.ts +30 -0
  50. package/dist/SearchInput.d.ts.map +1 -0
  51. package/dist/SearchInput.js +36 -0
  52. package/dist/SearchInput.js.map +1 -0
  53. package/dist/Section.d.ts +35 -0
  54. package/dist/Section.d.ts.map +1 -0
  55. package/dist/Section.js +31 -0
  56. package/dist/Section.js.map +1 -0
  57. package/dist/SegmentedTabs.d.ts +41 -0
  58. package/dist/SegmentedTabs.d.ts.map +1 -0
  59. package/dist/SegmentedTabs.js +33 -0
  60. package/dist/SegmentedTabs.js.map +1 -0
  61. package/dist/Select.d.ts +23 -0
  62. package/dist/Select.d.ts.map +1 -0
  63. package/dist/Select.js +23 -0
  64. package/dist/Select.js.map +1 -0
  65. package/dist/SettingRow.d.ts +33 -0
  66. package/dist/SettingRow.d.ts.map +1 -0
  67. package/dist/SettingRow.js +30 -0
  68. package/dist/SettingRow.js.map +1 -0
  69. package/dist/Switch.d.ts +32 -0
  70. package/dist/Switch.d.ts.map +1 -0
  71. package/dist/Switch.js +31 -0
  72. package/dist/Switch.js.map +1 -0
  73. package/dist/Tabs.d.ts +59 -0
  74. package/dist/Tabs.d.ts.map +1 -0
  75. package/dist/Tabs.js +51 -0
  76. package/dist/Tabs.js.map +1 -0
  77. package/dist/Textarea.d.ts +22 -0
  78. package/dist/Textarea.d.ts.map +1 -0
  79. package/dist/Textarea.js +25 -0
  80. package/dist/Textarea.js.map +1 -0
  81. package/dist/badge-classes.d.ts +15 -0
  82. package/dist/badge-classes.d.ts.map +1 -0
  83. package/dist/badge-classes.js +12 -0
  84. package/dist/badge-classes.js.map +1 -0
  85. package/dist/button-classes.d.ts +20 -0
  86. package/dist/button-classes.d.ts.map +1 -0
  87. package/dist/button-classes.js +16 -0
  88. package/dist/button-classes.js.map +1 -0
  89. package/dist/empty-state-classes.d.ts +12 -0
  90. package/dist/empty-state-classes.d.ts.map +1 -0
  91. package/dist/empty-state-classes.js +8 -0
  92. package/dist/empty-state-classes.js.map +1 -0
  93. package/dist/icon-button-classes.d.ts +15 -0
  94. package/dist/icon-button-classes.d.ts.map +1 -0
  95. package/dist/icon-button-classes.js +12 -0
  96. package/dist/icon-button-classes.js.map +1 -0
  97. package/dist/index.d.ts +33 -4
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +31 -4
  100. package/dist/index.js.map +1 -1
  101. package/dist/inline-edit-classes.d.ts +12 -0
  102. package/dist/inline-edit-classes.d.ts.map +1 -0
  103. package/dist/inline-edit-classes.js +18 -0
  104. package/dist/inline-edit-classes.js.map +1 -0
  105. package/dist/inline-edit-controller.d.ts +20 -0
  106. package/dist/inline-edit-controller.d.ts.map +1 -0
  107. package/dist/inline-edit-controller.js +52 -0
  108. package/dist/inline-edit-controller.js.map +1 -0
  109. package/dist/input-classes.d.ts +8 -0
  110. package/dist/input-classes.d.ts.map +1 -0
  111. package/dist/input-classes.js +10 -0
  112. package/dist/input-classes.js.map +1 -0
  113. package/dist/list-classes.d.ts +19 -0
  114. package/dist/list-classes.d.ts.map +1 -0
  115. package/dist/list-classes.js +16 -0
  116. package/dist/list-classes.js.map +1 -0
  117. package/dist/radio-card-classes.d.ts +8 -0
  118. package/dist/radio-card-classes.d.ts.map +1 -0
  119. package/dist/radio-card-classes.js +10 -0
  120. package/dist/radio-card-classes.js.map +1 -0
  121. package/dist/search-input-classes.d.ts +8 -0
  122. package/dist/search-input-classes.d.ts.map +1 -0
  123. package/dist/search-input-classes.js +10 -0
  124. package/dist/search-input-classes.js.map +1 -0
  125. package/dist/segmented-tabs-classes.d.ts +8 -0
  126. package/dist/segmented-tabs-classes.d.ts.map +1 -0
  127. package/dist/segmented-tabs-classes.js +10 -0
  128. package/dist/segmented-tabs-classes.js.map +1 -0
  129. package/dist/switch-classes.d.ts +8 -0
  130. package/dist/switch-classes.d.ts.map +1 -0
  131. package/dist/switch-classes.js +10 -0
  132. package/dist/switch-classes.js.map +1 -0
  133. package/dist/tabs-classes.d.ts +8 -0
  134. package/dist/tabs-classes.d.ts.map +1 -0
  135. package/dist/tabs-classes.js +10 -0
  136. package/dist/tabs-classes.js.map +1 -0
  137. package/package.json +1 -1
  138. package/src/AddRow.tsx +56 -0
  139. package/src/Badge.tsx +55 -0
  140. package/src/Button.tsx +52 -0
  141. package/src/Callout.tsx +26 -0
  142. package/src/CheckboxRow.tsx +54 -0
  143. package/src/EmptyState.tsx +69 -0
  144. package/src/IconButton.tsx +55 -0
  145. package/src/InlineEdit.tsx +260 -0
  146. package/src/Input.tsx +39 -0
  147. package/src/List.tsx +240 -0
  148. package/src/ModalActions.tsx +52 -0
  149. package/src/RadioGroup.tsx +101 -0
  150. package/src/SearchInput.tsx +98 -0
  151. package/src/Section.tsx +49 -0
  152. package/src/SegmentedTabs.tsx +70 -0
  153. package/src/Select.tsx +36 -0
  154. package/src/SettingRow.tsx +47 -0
  155. package/src/Switch.tsx +63 -0
  156. package/src/Tabs.tsx +89 -0
  157. package/src/Textarea.tsx +33 -0
  158. package/src/badge-classes.test.ts +20 -0
  159. package/src/badge-classes.ts +27 -0
  160. package/src/button-classes.test.ts +24 -0
  161. package/src/button-classes.ts +32 -0
  162. package/src/empty-state-classes.test.ts +13 -0
  163. package/src/empty-state-classes.ts +17 -0
  164. package/src/icon-button-classes.test.ts +13 -0
  165. package/src/icon-button-classes.ts +19 -0
  166. package/src/index.ts +45 -4
  167. package/src/inline-edit-classes.test.ts +27 -0
  168. package/src/inline-edit-classes.ts +18 -0
  169. package/src/inline-edit-controller.ts +53 -0
  170. package/src/input-classes.test.ts +13 -0
  171. package/src/input-classes.ts +9 -0
  172. package/src/list-classes.test.ts +23 -0
  173. package/src/list-classes.ts +26 -0
  174. package/src/radio-card-classes.test.ts +12 -0
  175. package/src/radio-card-classes.ts +9 -0
  176. package/src/search-input-classes.test.ts +12 -0
  177. package/src/search-input-classes.ts +9 -0
  178. package/src/segmented-tabs-classes.test.ts +12 -0
  179. package/src/segmented-tabs-classes.ts +9 -0
  180. package/src/switch-classes.test.ts +9 -0
  181. package/src/switch-classes.ts +9 -0
  182. package/src/tabs-classes.test.ts +12 -0
  183. package/src/tabs-classes.ts +9 -0
package/src/List.tsx ADDED
@@ -0,0 +1,240 @@
1
+ import {
2
+ Children,
3
+ cloneElement,
4
+ createContext,
5
+ isValidElement,
6
+ useContext,
7
+ type HTMLAttributes,
8
+ type KeyboardEvent,
9
+ type MouseEvent,
10
+ type ReactElement,
11
+ type ReactNode,
12
+ type Ref,
13
+ } from "react";
14
+ import { useFocusGroup, type FocusGroupItemProps } from "./use-focus-group";
15
+ import {
16
+ listRowDataSelected,
17
+ listRowNameTruncate,
18
+ type ListRowTruncate,
19
+ } from "./list-classes";
20
+
21
+ export type { ListRowTruncate };
22
+
23
+ interface ListRowInternalProps {
24
+ /** Injected by {@link List} — index in the focus group. */
25
+ __listIndex?: number;
26
+ /** Injected by {@link List} — focus-group item props to spread. */
27
+ __focusProps?: FocusGroupItemProps;
28
+ }
29
+
30
+ interface ListContextValue {
31
+ onActivate?: (index: number) => void;
32
+ }
33
+
34
+ const ListContext = createContext<ListContextValue | null>(null);
35
+
36
+ /**
37
+ * A full-width listbox of selectable rows. One tab stop; ↑/↓ move the focus
38
+ * ring, Space/Enter select. Pair with {@link ListRow} children.
39
+ *
40
+ * Styled purely via the host-provided `.silo-list` class — no stylesheet
41
+ * import is needed in the extension.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <List aria-label="Workspace folders">
46
+ * <ListRow
47
+ * selected={folder.primary}
48
+ * leading={<FolderIcon />}
49
+ * trailing={<Badge tone="accent">primary</Badge>}
50
+ * onSelect={() => choose(folder)}
51
+ * >
52
+ * {folder.path}
53
+ * </ListRow>
54
+ * </List>
55
+ * ```
56
+ *
57
+ * @category Consumer Services
58
+ * @public
59
+ */
60
+ export function List({
61
+ "aria-label": ariaLabel,
62
+ onActivate,
63
+ children,
64
+ }: {
65
+ /** Required — it's a listbox. */
66
+ "aria-label": string;
67
+ /** Enter / double-click on a row. */
68
+ onActivate?: (index: number) => void;
69
+ children?: ReactNode;
70
+ }) {
71
+ const rows = Children.toArray(children).filter(
72
+ isValidElement,
73
+ ) as ReactElement<ListRowProps & ListRowInternalProps>[];
74
+ const selectedIndex = rows.findIndex((row) => row.props.selected);
75
+ const group = useFocusGroup({
76
+ count: rows.length,
77
+ start: selectedIndex >= 0 ? selectedIndex : 0,
78
+ // Space/Enter both select (RFC 0016). Enter-only activation is layered
79
+ // on in ListRow's own keydown after the focus-group handler runs.
80
+ onActivate: (index) => {
81
+ rows[index]?.props.onSelect?.();
82
+ },
83
+ });
84
+
85
+ return (
86
+ <ListContext.Provider value={{ onActivate }}>
87
+ <div
88
+ role="listbox"
89
+ aria-label={ariaLabel}
90
+ className="silo-list"
91
+ {...group.containerProps}
92
+ >
93
+ {rows.map((row, index) =>
94
+ cloneElement(row, {
95
+ key: row.key ?? index,
96
+ __listIndex: index,
97
+ __focusProps: group.getItemProps(index),
98
+ }),
99
+ )}
100
+ </div>
101
+ </ListContext.Provider>
102
+ );
103
+ }
104
+
105
+ /**
106
+ * One row inside a {@link List}. Stretches full-width; long text truncates
107
+ * (use `truncate="start"` for paths). `trailing` content never shrinks.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <ListRow
112
+ * selected={folder.primary}
113
+ * leading={<FolderIcon />}
114
+ * trailing={<Badge tone="accent">primary</Badge>}
115
+ * truncate="start"
116
+ * onSelect={() => choose(folder)}
117
+ * >
118
+ * {folder.path}
119
+ * </ListRow>
120
+ * ```
121
+ *
122
+ * @category Consumer Services
123
+ * @public
124
+ */
125
+ export interface ListRowProps {
126
+ selected?: boolean;
127
+ /** Icon slot (dimmed, fixed). */
128
+ leading?: ReactNode;
129
+ /** Badge(s) and/or `IconButton size="sm"`(s). */
130
+ trailing?: ReactNode;
131
+ /** Default `"end"`. Use `"start"` for paths. */
132
+ truncate?: ListRowTruncate;
133
+ /** Click / Space select. */
134
+ onSelect?: () => void;
135
+ /** Enter / double-click. */
136
+ onActivate?: () => void;
137
+ children?: ReactNode;
138
+ }
139
+
140
+ /**
141
+ * @category Consumer Services
142
+ * @public
143
+ */
144
+ export function ListRow(props: ListRowProps) {
145
+ // List injects focus-group props via cloneElement; keep them off the public
146
+ // signature so they stay out of the generated API reference.
147
+ const {
148
+ selected = false,
149
+ leading,
150
+ trailing,
151
+ truncate = "end",
152
+ onSelect,
153
+ onActivate,
154
+ children,
155
+ __listIndex,
156
+ __focusProps,
157
+ } = props as ListRowProps & ListRowInternalProps;
158
+ const list = useContext(ListContext);
159
+ const dataSelected = listRowDataSelected(selected);
160
+ const truncateAttr = listRowNameTruncate(truncate);
161
+
162
+ const focusProps = __focusProps;
163
+ const {
164
+ ref,
165
+ onKeyDown: focusKeyDown,
166
+ onFocus,
167
+ onPointerDown,
168
+ tabIndex,
169
+ "data-focus-item": dataFocusItem,
170
+ "data-focus-visible": dataFocusVisible,
171
+ } = focusProps ?? {
172
+ ref: undefined as Ref<HTMLElement> | undefined,
173
+ onKeyDown: undefined,
174
+ onFocus: undefined,
175
+ onPointerDown: undefined,
176
+ tabIndex: -1 as number,
177
+ "data-focus-item": "" as const,
178
+ "data-focus-visible": undefined as "" | undefined,
179
+ };
180
+
181
+ function fireActivate() {
182
+ onActivate?.();
183
+ if (__listIndex != null) list?.onActivate?.(__listIndex);
184
+ }
185
+
186
+ const rowProps: HTMLAttributes<HTMLDivElement> & {
187
+ ref?: Ref<HTMLDivElement>;
188
+ "data-focus-item"?: "";
189
+ "data-focus-visible"?: "";
190
+ "data-selected"?: "true";
191
+ } = {
192
+ role: "option",
193
+ "aria-selected": selected,
194
+ className: "silo-list-row",
195
+ tabIndex,
196
+ ref: ref as Ref<HTMLDivElement> | undefined,
197
+ onFocus,
198
+ onPointerDown,
199
+ "data-focus-item": dataFocusItem,
200
+ ...(dataFocusVisible != null
201
+ ? { "data-focus-visible": dataFocusVisible }
202
+ : {}),
203
+ ...(dataSelected != null ? { "data-selected": dataSelected } : {}),
204
+ onClick: () => {
205
+ onSelect?.();
206
+ },
207
+ onDoubleClick: () => {
208
+ fireActivate();
209
+ },
210
+ onKeyDown: (e: KeyboardEvent<HTMLDivElement>) => {
211
+ focusKeyDown?.(e);
212
+ if (e.key === "Enter") fireActivate();
213
+ },
214
+ };
215
+
216
+ return (
217
+ <div {...rowProps}>
218
+ {leading != null && (
219
+ <span className="silo-list-row-leading">{leading}</span>
220
+ )}
221
+ <span
222
+ className="silo-list-row-name"
223
+ {...(truncateAttr != null ? { "data-truncate": truncateAttr } : {})}
224
+ >
225
+ {children}
226
+ </span>
227
+ {trailing != null && (
228
+ <span
229
+ className="silo-list-row-trailing"
230
+ onClick={(e: MouseEvent) => {
231
+ // Trailing IconButtons own their clicks — don't select the row.
232
+ e.stopPropagation();
233
+ }}
234
+ >
235
+ {trailing}
236
+ </span>
237
+ )}
238
+ </div>
239
+ );
240
+ }
@@ -0,0 +1,52 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The right-aligned footer row for a modal's action buttons — a thin
5
+ * `.silo-modal-actions` wrapper so every modal's footer lines up without each
6
+ * caller re-specifying the flex row. Optional `start` slot pins meta text or
7
+ * a secondary action on the left.
8
+ *
9
+ * Styled purely via host-provided `.silo-modal-actions*` classes — no
10
+ * stylesheet import is needed in the extension.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <ModalActions>
15
+ * <Button onClick={close}>Cancel</Button>
16
+ * <Button variant="primary" onClick={save}>Create</Button>
17
+ * </ModalActions>
18
+ *
19
+ * <ModalActions start="6 sessions · 6 procs">
20
+ * <Button>Go to Terminal</Button>
21
+ * <Button variant="danger">End Task</Button>
22
+ * </ModalActions>
23
+ *
24
+ * <ModalActions
25
+ * start={<Button size="sm" onClick={create}>+ Create branch</Button>}
26
+ * >
27
+ * <Button onClick={fetch}>Fetch</Button>
28
+ * </ModalActions>
29
+ * ```
30
+ *
31
+ * @category Consumer Services
32
+ * @public
33
+ */
34
+ export function ModalActions({
35
+ start,
36
+ children,
37
+ }: {
38
+ /**
39
+ * Optional left-pinned slot for meta text or a secondary action.
40
+ */
41
+ start?: ReactNode;
42
+ children?: ReactNode;
43
+ }) {
44
+ return (
45
+ <div className="silo-modal-actions">
46
+ {start != null && start !== false && (
47
+ <div className="silo-modal-actions-start">{start}</div>
48
+ )}
49
+ {children}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,101 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import { radioCardDataSelected } from "./radio-card-classes";
3
+
4
+ const RadioGroupContext = createContext<{
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ } | null>(null);
8
+
9
+ /**
10
+ * Stacked option cards — each a full-width click target with a radio dot, a
11
+ * bold title, and a dim description. Wrap {@link RadioCard} children; the
12
+ * selected card gets an accent border and a faint accent tint. Native tab
13
+ * stops per card — no arrow-key roving.
14
+ *
15
+ * Styled purely via host-provided `.silo-radio-card*` classes — no
16
+ * stylesheet import is needed in the extension.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * <RadioGroup value={mode} onChange={setMode}>
21
+ * <RadioCard
22
+ * value="clear"
23
+ * title="Clear the finished indicator"
24
+ * description="Viewing the terminal acknowledges the run — the green check disappears."
25
+ * />
26
+ * <RadioCard
27
+ * value="keep"
28
+ * title="Keep it until the next run"
29
+ * description="Viewing changes nothing."
30
+ * />
31
+ * </RadioGroup>
32
+ * ```
33
+ *
34
+ * @category Consumer Services
35
+ * @public
36
+ */
37
+ export function RadioGroup({
38
+ value,
39
+ onChange,
40
+ children,
41
+ }: {
42
+ value: string;
43
+ onChange: (value: string) => void;
44
+ children?: ReactNode;
45
+ }) {
46
+ return (
47
+ <RadioGroupContext.Provider value={{ value, onChange }}>
48
+ {children}
49
+ </RadioGroupContext.Provider>
50
+ );
51
+ }
52
+
53
+ /**
54
+ * One option inside a {@link RadioGroup}. The whole card is the click target.
55
+ *
56
+ * @example
57
+ * ```tsx
58
+ * <RadioCard
59
+ * value="clear"
60
+ * title="Clear the finished indicator"
61
+ * description="Viewing the terminal acknowledges the run."
62
+ * />
63
+ * ```
64
+ *
65
+ * @category Consumer Services
66
+ * @public
67
+ */
68
+ export function RadioCard({
69
+ value,
70
+ title,
71
+ description,
72
+ }: {
73
+ value: string;
74
+ title: string;
75
+ description?: string;
76
+ }) {
77
+ const group = useContext(RadioGroupContext);
78
+ if (!group) {
79
+ throw new Error("RadioCard must be used inside a RadioGroup");
80
+ }
81
+ const selected = group.value === value;
82
+ const dataSelected = radioCardDataSelected(selected);
83
+ return (
84
+ <button
85
+ type="button"
86
+ role="radio"
87
+ aria-checked={selected}
88
+ className="silo-radio-card"
89
+ {...(dataSelected != null ? { "data-selected": dataSelected } : {})}
90
+ onClick={() => group.onChange(value)}
91
+ >
92
+ <span className="silo-radio-card-dot" aria-hidden="true" />
93
+ <span>
94
+ <span className="silo-radio-card-title">{title}</span>
95
+ {description != null && (
96
+ <span className="silo-radio-card-desc">{description}</span>
97
+ )}
98
+ </span>
99
+ </button>
100
+ );
101
+ }
@@ -0,0 +1,98 @@
1
+ import type { ReactNode } from "react";
2
+ import { searchInputClass } from "./search-input-classes";
3
+
4
+ function SearchIcon() {
5
+ return (
6
+ <svg
7
+ className="silo-search-input-icon"
8
+ width="13"
9
+ height="13"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ aria-hidden="true"
13
+ >
14
+ <circle cx="7" cy="7" r="5.2" stroke="currentColor" strokeWidth="1.4" />
15
+ <path
16
+ d="M11 11l3.5 3.5"
17
+ stroke="currentColor"
18
+ strokeWidth="1.4"
19
+ strokeLinecap="round"
20
+ />
21
+ </svg>
22
+ );
23
+ }
24
+
25
+ function ClearIcon() {
26
+ return (
27
+ <svg width="12" height="12" viewBox="0 0 16 16" aria-hidden="true">
28
+ <path
29
+ d="M4 4l8 8M12 4l-8 8"
30
+ stroke="currentColor"
31
+ strokeWidth="1.5"
32
+ strokeLinecap="round"
33
+ />
34
+ </svg>
35
+ );
36
+ }
37
+
38
+ /**
39
+ * The filter-as-you-type field: leading search icon, and a clear ✕ that
40
+ * appears once there's a value (and stays a real tab stop). Pair with
41
+ * {@link List} for the standard picker pattern.
42
+ *
43
+ * Styled purely via host-provided `.silo-search-input*` classes — no
44
+ * stylesheet import is needed in the extension.
45
+ *
46
+ * @example
47
+ * ```tsx
48
+ * <SearchInput
49
+ * value={query}
50
+ * onValueChange={setQuery}
51
+ * placeholder="Filter branches…"
52
+ * />
53
+ * ```
54
+ *
55
+ * @category Consumer Services
56
+ * @public
57
+ */
58
+ export function SearchInput({
59
+ value,
60
+ onValueChange,
61
+ placeholder,
62
+ autoFocus,
63
+ onClear,
64
+ }: {
65
+ value: string;
66
+ onValueChange: (value: string) => void;
67
+ placeholder?: string;
68
+ autoFocus?: boolean;
69
+ /** Extra hook when ✕ clears the field. */
70
+ onClear?: () => void;
71
+ }): ReactNode {
72
+ const hasValue = value.length > 0;
73
+ return (
74
+ <div className={searchInputClass(hasValue)}>
75
+ <SearchIcon />
76
+ <input
77
+ type="text"
78
+ value={value}
79
+ placeholder={placeholder}
80
+ autoFocus={autoFocus}
81
+ onChange={(e) => onValueChange(e.target.value)}
82
+ autoComplete="off"
83
+ />
84
+ <button
85
+ type="button"
86
+ className="silo-search-input-clear"
87
+ aria-label="Clear"
88
+ tabIndex={hasValue ? 0 : -1}
89
+ onClick={() => {
90
+ onValueChange("");
91
+ onClear?.();
92
+ }}
93
+ >
94
+ <ClearIcon />
95
+ </button>
96
+ </div>
97
+ );
98
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * A labeled group — the uppercase `NAME` / `FOLDERS` / `FORMATTING` headers
5
+ * seen throughout Silo's modals and settings pages.
6
+ *
7
+ * Styled purely via host-provided `.silo-section*` classes — no stylesheet
8
+ * import is needed in the extension.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Section label="Formatting">
13
+ * <SettingRow
14
+ * label="Format on save"
15
+ * hint="Run Format Document before writing to disk."
16
+ * >
17
+ * <Switch
18
+ * checked={formatOnSave}
19
+ * onChange={setFormatOnSave}
20
+ * aria-label="Format on save"
21
+ * />
22
+ * </SettingRow>
23
+ * </Section>
24
+ * ```
25
+ *
26
+ * @category Consumer Services
27
+ * @public
28
+ */
29
+ export function Section({
30
+ label,
31
+ accessory,
32
+ children,
33
+ }: {
34
+ /** Rendered uppercase, letter-spaced, chrome−1, `text-lo`. */
35
+ label: string;
36
+ /** Right-aligned — a count badge, an add affordance. */
37
+ accessory?: ReactNode;
38
+ children?: ReactNode;
39
+ }) {
40
+ return (
41
+ <div className="silo-section">
42
+ <div className="silo-section-header">
43
+ <span className="silo-section-label">{label}</span>
44
+ {accessory}
45
+ </div>
46
+ {children}
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,70 @@
1
+ import type { ReactNode } from "react";
2
+ import { segmentedTabDataActive } from "./segmented-tabs-classes";
3
+
4
+ /**
5
+ * One segment in a {@link SegmentedTabs} strip.
6
+ *
7
+ * @category Core Types
8
+ * @public
9
+ */
10
+ export interface SegmentedTabItem<T extends string = string> {
11
+ id: T;
12
+ label: string;
13
+ icon?: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * A pill riding in a recessed well — self-contained, no relationship to
18
+ * content below it, so it can sit in a header row next to other controls.
19
+ * Native tab stops per segment; no arrow-key roving.
20
+ *
21
+ * Styled purely via host-provided `.silo-segmented*` classes — no stylesheet
22
+ * import is needed in the extension.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * <SegmentedTabs
27
+ * tabs={[
28
+ * { id: "browse", label: "Browse" },
29
+ * { id: "installed", label: "Installed" },
30
+ * ]}
31
+ * active={tab}
32
+ * onSelect={setTab}
33
+ * />
34
+ * ```
35
+ *
36
+ * @category Consumer Services
37
+ * @public
38
+ */
39
+ export function SegmentedTabs<T extends string>({
40
+ tabs,
41
+ active,
42
+ onSelect,
43
+ }: {
44
+ tabs: SegmentedTabItem<T>[];
45
+ active: T;
46
+ onSelect: (id: T) => void;
47
+ }) {
48
+ return (
49
+ <div className="silo-segmented" role="tablist">
50
+ {tabs.map((tab) => {
51
+ const isActive = tab.id === active;
52
+ const dataActive = segmentedTabDataActive(isActive);
53
+ return (
54
+ <button
55
+ key={tab.id}
56
+ type="button"
57
+ role="tab"
58
+ aria-selected={isActive}
59
+ className="silo-segmented-tab"
60
+ {...(dataActive != null ? { "data-active": dataActive } : {})}
61
+ onClick={() => onSelect(tab.id)}
62
+ >
63
+ {tab.icon}
64
+ {tab.label}
65
+ </button>
66
+ );
67
+ })}
68
+ </div>
69
+ );
70
+ }
package/src/Select.tsx ADDED
@@ -0,0 +1,36 @@
1
+ import type { SelectHTMLAttributes, ReactNode } from "react";
2
+
3
+ /**
4
+ * A native `<select>` wearing the {@link Input} treatment — keep it for short
5
+ * enumerable values ("Block / Bar / Underline").
6
+ *
7
+ * Styled purely via the host-provided `.silo-select` class — no stylesheet
8
+ * import is needed in the extension.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Select value={cursorStyle} onChange={(e) => setCursorStyle(e.target.value)}>
13
+ * <option value="block">Block</option>
14
+ * <option value="bar">Bar</option>
15
+ * </Select>
16
+ * ```
17
+ *
18
+ * @category Consumer Services
19
+ * @public
20
+ */
21
+ export function Select({
22
+ className,
23
+ children,
24
+ ...rest
25
+ }: {
26
+ children?: ReactNode;
27
+ } & Omit<SelectHTMLAttributes<HTMLSelectElement>, "children">) {
28
+ return (
29
+ <select
30
+ className={className ? `silo-select ${className}` : "silo-select"}
31
+ {...rest}
32
+ >
33
+ {children}
34
+ </select>
35
+ );
36
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * The label + hint + control row every settings surface uses: title and dim
5
+ * description on the left, the control pinned right. Rows in a group are
6
+ * separated by hairline dividers.
7
+ *
8
+ * Styled purely via host-provided `.silo-setting-row*` classes — no
9
+ * stylesheet import is needed in the extension.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <SettingRow
14
+ * label="Format on save"
15
+ * hint="Run Format Document before writing to disk."
16
+ * >
17
+ * <Switch
18
+ * checked={formatOnSave}
19
+ * onChange={setFormatOnSave}
20
+ * aria-label="Format on save"
21
+ * />
22
+ * </SettingRow>
23
+ * ```
24
+ *
25
+ * @category Consumer Services
26
+ * @public
27
+ */
28
+ export function SettingRow({
29
+ label,
30
+ hint,
31
+ children,
32
+ }: {
33
+ label: string;
34
+ hint?: string;
35
+ /** The control — `Switch`, `Select`, `Input`, etc. */
36
+ children?: ReactNode;
37
+ }) {
38
+ return (
39
+ <div className="silo-setting-row">
40
+ <div className="silo-setting-row-text">
41
+ <div className="silo-setting-row-label">{label}</div>
42
+ {hint != null && <div className="silo-setting-row-hint">{hint}</div>}
43
+ </div>
44
+ <div className="silo-setting-row-control">{children}</div>
45
+ </div>
46
+ );
47
+ }