@silo-code/sdk 0.28.0 → 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.
- package/dist/AddRow.d.ts +21 -0
- package/dist/AddRow.d.ts.map +1 -0
- package/dist/AddRow.js +24 -0
- package/dist/AddRow.js.map +1 -0
- package/dist/Badge.d.ts +32 -0
- package/dist/Badge.d.ts.map +1 -0
- package/dist/Badge.js +32 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Button.d.ts +29 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +25 -0
- package/dist/Button.js.map +1 -0
- package/dist/Callout.d.ts +26 -0
- package/dist/Callout.d.ts.map +1 -0
- package/dist/Callout.js +26 -0
- package/dist/Callout.js.map +1 -0
- package/dist/CheckboxRow.d.ts +27 -0
- package/dist/CheckboxRow.d.ts.map +1 -0
- package/dist/CheckboxRow.js +24 -0
- package/dist/CheckboxRow.js.map +1 -0
- package/dist/EmptyState.d.ts +40 -0
- package/dist/EmptyState.d.ts.map +1 -0
- package/dist/EmptyState.js +34 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/IconButton.d.ts +35 -0
- package/dist/IconButton.d.ts.map +1 -0
- package/dist/IconButton.js +31 -0
- package/dist/IconButton.js.map +1 -0
- package/dist/InlineEdit.d.ts +63 -0
- package/dist/InlineEdit.d.ts.map +1 -0
- package/dist/InlineEdit.js +121 -0
- package/dist/InlineEdit.js.map +1 -0
- package/dist/Input.d.ts +26 -0
- package/dist/Input.d.ts.map +1 -0
- package/dist/Input.js +28 -0
- package/dist/Input.js.map +1 -0
- package/dist/List.d.ts +74 -0
- package/dist/List.d.ts.map +1 -0
- package/dist/List.js +104 -0
- package/dist/List.js.map +1 -0
- package/dist/ModalActions.d.ts +40 -0
- package/dist/ModalActions.d.ts.map +1 -0
- package/dist/ModalActions.js +36 -0
- package/dist/ModalActions.js.map +1 -0
- package/dist/RadioGroup.d.ts +55 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +60 -0
- package/dist/RadioGroup.js.map +1 -0
- package/dist/SearchInput.d.ts +30 -0
- package/dist/SearchInput.d.ts.map +1 -0
- package/dist/SearchInput.js +36 -0
- package/dist/SearchInput.js.map +1 -0
- package/dist/Section.d.ts +35 -0
- package/dist/Section.d.ts.map +1 -0
- package/dist/Section.js +31 -0
- package/dist/Section.js.map +1 -0
- package/dist/SegmentedTabs.d.ts +41 -0
- package/dist/SegmentedTabs.d.ts.map +1 -0
- package/dist/SegmentedTabs.js +33 -0
- package/dist/SegmentedTabs.js.map +1 -0
- package/dist/Select.d.ts +23 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +23 -0
- package/dist/Select.js.map +1 -0
- package/dist/SettingRow.d.ts +33 -0
- package/dist/SettingRow.d.ts.map +1 -0
- package/dist/SettingRow.js +30 -0
- package/dist/SettingRow.js.map +1 -0
- package/dist/Switch.d.ts +32 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +31 -0
- package/dist/Switch.js.map +1 -0
- package/dist/Tabs.d.ts +59 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +51 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/Textarea.d.ts +22 -0
- package/dist/Textarea.d.ts.map +1 -0
- package/dist/Textarea.js +25 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/badge-classes.d.ts +15 -0
- package/dist/badge-classes.d.ts.map +1 -0
- package/dist/badge-classes.js +12 -0
- package/dist/badge-classes.js.map +1 -0
- package/dist/button-classes.d.ts +20 -0
- package/dist/button-classes.d.ts.map +1 -0
- package/dist/button-classes.js +16 -0
- package/dist/button-classes.js.map +1 -0
- package/dist/editor-service.d.ts +9 -3
- package/dist/editor-service.d.ts.map +1 -1
- package/dist/empty-state-classes.d.ts +12 -0
- package/dist/empty-state-classes.d.ts.map +1 -0
- package/dist/empty-state-classes.js +8 -0
- package/dist/empty-state-classes.js.map +1 -0
- package/dist/icon-button-classes.d.ts +15 -0
- package/dist/icon-button-classes.d.ts.map +1 -0
- package/dist/icon-button-classes.js +12 -0
- package/dist/icon-button-classes.js.map +1 -0
- package/dist/index.d.ts +33 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -4
- package/dist/index.js.map +1 -1
- package/dist/inline-edit-classes.d.ts +12 -0
- package/dist/inline-edit-classes.d.ts.map +1 -0
- package/dist/inline-edit-classes.js +18 -0
- package/dist/inline-edit-classes.js.map +1 -0
- package/dist/inline-edit-controller.d.ts +20 -0
- package/dist/inline-edit-controller.d.ts.map +1 -0
- package/dist/inline-edit-controller.js +52 -0
- package/dist/inline-edit-controller.js.map +1 -0
- package/dist/input-classes.d.ts +8 -0
- package/dist/input-classes.d.ts.map +1 -0
- package/dist/input-classes.js +10 -0
- package/dist/input-classes.js.map +1 -0
- package/dist/list-classes.d.ts +19 -0
- package/dist/list-classes.d.ts.map +1 -0
- package/dist/list-classes.js +16 -0
- package/dist/list-classes.js.map +1 -0
- package/dist/radio-card-classes.d.ts +8 -0
- package/dist/radio-card-classes.d.ts.map +1 -0
- package/dist/radio-card-classes.js +10 -0
- package/dist/radio-card-classes.js.map +1 -0
- package/dist/search-input-classes.d.ts +8 -0
- package/dist/search-input-classes.d.ts.map +1 -0
- package/dist/search-input-classes.js +10 -0
- package/dist/search-input-classes.js.map +1 -0
- package/dist/segmented-tabs-classes.d.ts +8 -0
- package/dist/segmented-tabs-classes.d.ts.map +1 -0
- package/dist/segmented-tabs-classes.js +10 -0
- package/dist/segmented-tabs-classes.js.map +1 -0
- package/dist/switch-classes.d.ts +8 -0
- package/dist/switch-classes.d.ts.map +1 -0
- package/dist/switch-classes.js +10 -0
- package/dist/switch-classes.js.map +1 -0
- package/dist/tabs-classes.d.ts +8 -0
- package/dist/tabs-classes.d.ts.map +1 -0
- package/dist/tabs-classes.js +10 -0
- package/dist/tabs-classes.js.map +1 -0
- package/package.json +1 -1
- package/src/AddRow.tsx +56 -0
- package/src/Badge.tsx +55 -0
- package/src/Button.tsx +52 -0
- package/src/Callout.tsx +26 -0
- package/src/CheckboxRow.tsx +54 -0
- package/src/EmptyState.tsx +69 -0
- package/src/IconButton.tsx +55 -0
- package/src/InlineEdit.tsx +260 -0
- package/src/Input.tsx +39 -0
- package/src/List.tsx +240 -0
- package/src/ModalActions.tsx +52 -0
- package/src/RadioGroup.tsx +101 -0
- package/src/SearchInput.tsx +98 -0
- package/src/Section.tsx +49 -0
- package/src/SegmentedTabs.tsx +70 -0
- package/src/Select.tsx +36 -0
- package/src/SettingRow.tsx +47 -0
- package/src/Switch.tsx +63 -0
- package/src/Tabs.tsx +89 -0
- package/src/Textarea.tsx +33 -0
- package/src/badge-classes.test.ts +20 -0
- package/src/badge-classes.ts +27 -0
- package/src/button-classes.test.ts +24 -0
- package/src/button-classes.ts +32 -0
- package/src/editor-service.ts +9 -3
- package/src/empty-state-classes.test.ts +13 -0
- package/src/empty-state-classes.ts +17 -0
- package/src/icon-button-classes.test.ts +13 -0
- package/src/icon-button-classes.ts +19 -0
- package/src/index.ts +45 -4
- package/src/inline-edit-classes.test.ts +27 -0
- package/src/inline-edit-classes.ts +18 -0
- package/src/inline-edit-controller.ts +53 -0
- package/src/input-classes.test.ts +13 -0
- package/src/input-classes.ts +9 -0
- package/src/list-classes.test.ts +23 -0
- package/src/list-classes.ts +26 -0
- package/src/radio-card-classes.test.ts +12 -0
- package/src/radio-card-classes.ts +9 -0
- package/src/search-input-classes.test.ts +12 -0
- package/src/search-input-classes.ts +9 -0
- package/src/segmented-tabs-classes.test.ts +12 -0
- package/src/segmented-tabs-classes.ts +9 -0
- package/src/switch-classes.test.ts +9 -0
- package/src/switch-classes.ts +9 -0
- package/src/tabs-classes.test.ts +12 -0
- 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
|
+
}
|
package/src/Section.tsx
ADDED
|
@@ -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
|
+
}
|