@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/AddRow.tsx ADDED
@@ -0,0 +1,56 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+
3
+ function PlusIcon() {
4
+ return (
5
+ <svg
6
+ className="silo-add-row-plus"
7
+ width="12"
8
+ height="12"
9
+ viewBox="0 0 16 16"
10
+ aria-hidden="true"
11
+ >
12
+ <path
13
+ d="M8 2v12M2 8h12"
14
+ stroke="currentColor"
15
+ strokeWidth="1.5"
16
+ strokeLinecap="round"
17
+ />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ /**
23
+ * The ghost "+ Add…" action that sits flush under a {@link List} — visually
24
+ * a row, semantically a button. No fill at rest; `bg-hover` on hover.
25
+ *
26
+ * Styled purely via host-provided `.silo-add-row*` classes — no stylesheet
27
+ * import is needed in the extension.
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * <List aria-label="Folders">…</List>
32
+ * <AddRow onClick={addFolder}>Add Folder…</AddRow>
33
+ * ```
34
+ *
35
+ * @category Consumer Services
36
+ * @public
37
+ */
38
+ export function AddRow({
39
+ children,
40
+ className,
41
+ type = "button",
42
+ ...rest
43
+ }: {
44
+ children?: ReactNode;
45
+ } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">) {
46
+ return (
47
+ <button
48
+ type={type}
49
+ className={className ? `silo-add-row ${className}` : "silo-add-row"}
50
+ {...rest}
51
+ >
52
+ <PlusIcon />
53
+ {children}
54
+ </button>
55
+ );
56
+ }
package/src/Badge.tsx ADDED
@@ -0,0 +1,55 @@
1
+ import type { CSSProperties, HTMLAttributes, ReactNode } from "react";
2
+ import { badgeClass, type BadgeTone } from "./badge-classes";
3
+
4
+ export type { BadgeTone };
5
+
6
+ /**
7
+ * A small pill for status or identity — on a {@link ListRow}'s trailing
8
+ * slot, in a {@link SettingRow}'s control slot, next to a title. An
9
+ * arbitrary `color` overrides `tone` for identity colors (e.g. workspace
10
+ * group swatches) via the `--badge-color` custom property.
11
+ *
12
+ * Styled purely via host-provided `.silo-badge*` classes — no stylesheet
13
+ * import is needed in the extension.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <Badge tone="ok">Installed</Badge>
18
+ * <Badge tone="warn">Update available</Badge>
19
+ * <Badge tone="accent">current</Badge>
20
+ * <Badge>primary</Badge>
21
+ * <Badge tone="outline">Silo</Badge>
22
+ * <Badge color="#e06c75">Frontend</Badge>
23
+ * ```
24
+ *
25
+ * @category Consumer Services
26
+ * @public
27
+ */
28
+ export function Badge({
29
+ tone = "neutral",
30
+ color,
31
+ className,
32
+ style,
33
+ children,
34
+ ...rest
35
+ }: {
36
+ tone?: BadgeTone;
37
+ /** Arbitrary CSS color — overrides `tone` for identity colors. */
38
+ color?: string;
39
+ children?: ReactNode;
40
+ } & Omit<HTMLAttributes<HTMLSpanElement>, "children" | "color">) {
41
+ const classes = badgeClass(tone, color);
42
+ const mergedStyle: CSSProperties | undefined =
43
+ color != null
44
+ ? ({ ...style, ["--badge-color" as string]: color } as CSSProperties)
45
+ : style;
46
+ return (
47
+ <span
48
+ className={className ? `${classes} ${className}` : classes}
49
+ style={mergedStyle}
50
+ {...rest}
51
+ >
52
+ {children}
53
+ </span>
54
+ );
55
+ }
package/src/Button.tsx ADDED
@@ -0,0 +1,52 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ import {
3
+ buttonClass,
4
+ type ButtonSize,
5
+ type ButtonVariant,
6
+ } from "./button-classes";
7
+
8
+ export type { ButtonSize, ButtonVariant };
9
+
10
+ /**
11
+ * The action button in three variants. In a modal footer the primary action
12
+ * sits rightmost with neutral actions to its left (see {@link ModalActions}).
13
+ *
14
+ * Styled purely via host-provided `.silo-button*` classes — no stylesheet
15
+ * import is needed in the extension.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <Button onClick={cancel}>Cancel</Button>
20
+ * <Button variant="primary" onClick={save}>Save</Button>
21
+ * <Button variant="danger" onClick={remove}>Delete</Button>
22
+ * <Button size="sm">Compact</Button>
23
+ * ```
24
+ *
25
+ * @category Consumer Services
26
+ * @public
27
+ */
28
+ export function Button({
29
+ variant = "normal",
30
+ size = "normal",
31
+ className,
32
+ type = "button",
33
+ children,
34
+ ...rest
35
+ }: {
36
+ /** `primary` = the accent-filled main action; `danger` = destructive. */
37
+ variant?: ButtonVariant;
38
+ /** `sm` for inline/compact contexts (e.g. a footer `start` slot). */
39
+ size?: ButtonSize;
40
+ children?: ReactNode;
41
+ } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children">) {
42
+ const classes = buttonClass(variant, size);
43
+ return (
44
+ <button
45
+ type={type}
46
+ className={className ? `${classes} ${className}` : classes}
47
+ {...rest}
48
+ >
49
+ {children}
50
+ </button>
51
+ );
52
+ }
@@ -0,0 +1,26 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * A quiet set-off box for explanatory copy — a hairline border around body
5
+ * text, no fill, no tone. Not a warning/error/info alert (there is
6
+ * deliberately no `tone`): status belongs to {@link Badge} tones and
7
+ * {@link EmptyState}.
8
+ *
9
+ * Styled purely via the host-provided `.silo-callout` class — no stylesheet
10
+ * import is needed in the extension.
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * <Callout>
15
+ * Opening a worktree adds it as another folder in this workspace — its
16
+ * files, terminals, and Git panel appear alongside your current ones.
17
+ * Closing a view leaves the worktree untouched on disk.
18
+ * </Callout>
19
+ * ```
20
+ *
21
+ * @category Consumer Services
22
+ * @public
23
+ */
24
+ export function Callout({ children }: { children?: ReactNode }) {
25
+ return <div className="silo-callout">{children}</div>;
26
+ }
@@ -0,0 +1,54 @@
1
+ import type { InputHTMLAttributes } from "react";
2
+
3
+ /**
4
+ * A labeled checkbox row (15px box, accent check). The whole label is the
5
+ * click target.
6
+ *
7
+ * Styled purely via the host-provided `.silo-checkbox-row` class — no
8
+ * stylesheet import is needed in the extension.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <CheckboxRow
13
+ * label="Only monitor the checked-out branch"
14
+ * checked={onlyCheckedOut}
15
+ * onChange={setOnlyCheckedOut}
16
+ * />
17
+ * ```
18
+ *
19
+ * @category Consumer Services
20
+ * @public
21
+ */
22
+ export function CheckboxRow({
23
+ label,
24
+ checked,
25
+ onChange,
26
+ disabled,
27
+ className,
28
+ ...rest
29
+ }: {
30
+ label: string;
31
+ checked: boolean;
32
+ onChange: (checked: boolean) => void;
33
+ disabled?: boolean;
34
+ } & Omit<
35
+ InputHTMLAttributes<HTMLInputElement>,
36
+ "type" | "checked" | "onChange" | "disabled" | "children"
37
+ >) {
38
+ return (
39
+ <label
40
+ className={
41
+ className ? `silo-checkbox-row ${className}` : "silo-checkbox-row"
42
+ }
43
+ >
44
+ <input
45
+ type="checkbox"
46
+ checked={checked}
47
+ disabled={disabled}
48
+ onChange={(e) => onChange(e.target.checked)}
49
+ {...rest}
50
+ />
51
+ {label}
52
+ </label>
53
+ );
54
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from "react";
2
+ import {
3
+ emptyStateIconDataTone,
4
+ type EmptyStateTone,
5
+ } from "./empty-state-classes";
6
+
7
+ export type { EmptyStateTone };
8
+
9
+ /**
10
+ * The big-icon + title + dim-description block a modal shows when a list is
11
+ * empty or everything is fine ("All workflows passing"). Renders flat in the
12
+ * modal body — no card, no shadow of its own.
13
+ *
14
+ * Styled purely via host-provided `.silo-empty-state*` classes — no
15
+ * stylesheet import is needed in the extension.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <EmptyState
20
+ * tone="ok"
21
+ * icon={<CheckIcon />}
22
+ * title="All workflows passing"
23
+ * description="No failures or active runs on this repo."
24
+ * />
25
+ *
26
+ * <EmptyState
27
+ * icon={<SearchIcon />}
28
+ * title="No matching branches"
29
+ * description="Try a different filter."
30
+ * />
31
+ * ```
32
+ *
33
+ * @category Consumer Services
34
+ * @public
35
+ */
36
+ export function EmptyState({
37
+ tone = "neutral",
38
+ icon,
39
+ title,
40
+ description,
41
+ action,
42
+ }: {
43
+ /** Colors the circled icon; `ok` = green ring for positive-empty. */
44
+ tone?: EmptyStateTone;
45
+ icon?: ReactNode;
46
+ title: string;
47
+ description?: string;
48
+ /** e.g. a `Button` ("Clear filter"). */
49
+ action?: ReactNode;
50
+ }) {
51
+ const dataTone = emptyStateIconDataTone(tone);
52
+ return (
53
+ <div className="silo-empty-state">
54
+ {icon != null && (
55
+ <div
56
+ className="silo-empty-state-icon"
57
+ {...(dataTone != null ? { "data-tone": dataTone } : {})}
58
+ >
59
+ {icon}
60
+ </div>
61
+ )}
62
+ <div className="silo-empty-state-title">{title}</div>
63
+ {description != null && (
64
+ <div className="silo-empty-state-desc">{description}</div>
65
+ )}
66
+ {action}
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,55 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ import { iconButtonClass, type IconButtonSize } from "./icon-button-classes";
3
+
4
+ export type { IconButtonSize };
5
+
6
+ /**
7
+ * A square icon-only button — the kit's one answer for ✕, ⋮, ↻, ✏️, and
8
+ * friends. `aria-label` is **required**: the icon is the only visual, so the
9
+ * label is the accessible name. Pair with {@link Tooltip} so sighted users
10
+ * get the same label on hover.
11
+ *
12
+ * Styled purely via host-provided `.silo-icon-button*` classes — no
13
+ * stylesheet import is needed in the extension.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <IconButton aria-label="Refresh" onClick={refresh}>
18
+ * <RefreshIcon />
19
+ * </IconButton>
20
+ *
21
+ * // compact — e.g. in a ListRow's trailing slot
22
+ * <IconButton size="sm" aria-label="Pin" onClick={pin}>
23
+ * <PinIcon />
24
+ * </IconButton>
25
+ * ```
26
+ *
27
+ * @category Consumer Services
28
+ * @public
29
+ */
30
+ export function IconButton({
31
+ size = "normal",
32
+ className,
33
+ type = "button",
34
+ children,
35
+ "aria-label": ariaLabel,
36
+ ...rest
37
+ }: {
38
+ /** 32px standalone / 26px for tight or inline contexts. */
39
+ size?: IconButtonSize;
40
+ /** Required — the icon is the only visual. */
41
+ "aria-label": string;
42
+ children?: ReactNode;
43
+ } & Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children" | "aria-label">) {
44
+ const classes = iconButtonClass(size);
45
+ return (
46
+ <button
47
+ type={type}
48
+ aria-label={ariaLabel}
49
+ className={className ? `${classes} ${className}` : classes}
50
+ {...rest}
51
+ >
52
+ {children}
53
+ </button>
54
+ );
55
+ }
@@ -0,0 +1,260 @@
1
+ import {
2
+ useEffect,
3
+ useLayoutEffect,
4
+ useRef,
5
+ useState,
6
+ type KeyboardEvent,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { IconButton } from "./IconButton";
10
+ import { Input } from "./Input";
11
+ import { Textarea } from "./Textarea";
12
+ import { Tooltip } from "./Tooltip";
13
+ import {
14
+ inlineEditDisplayClass,
15
+ inlineEditRowClass,
16
+ } from "./inline-edit-classes";
17
+ import { setActiveInlineEditCancel } from "./inline-edit-controller";
18
+
19
+ /**
20
+ * Result of an {@link InlineEdit} `validate` callback.
21
+ *
22
+ * @category Core Types
23
+ * @public
24
+ */
25
+ export type InlineEditValidation =
26
+ | { ok: true; value: string }
27
+ | { ok: false; error: string };
28
+
29
+ function PencilIcon() {
30
+ return (
31
+ <svg
32
+ width="13"
33
+ height="13"
34
+ viewBox="0 0 16 16"
35
+ fill="none"
36
+ aria-hidden="true"
37
+ >
38
+ <path
39
+ d="M11.1 2.4a1.4 1.4 0 0 1 2 2L5.5 12l-2.8.8.8-2.8 7.6-7.6z"
40
+ stroke="currentColor"
41
+ strokeWidth="1.2"
42
+ strokeLinejoin="round"
43
+ />
44
+ </svg>
45
+ );
46
+ }
47
+
48
+ function CheckIcon() {
49
+ return (
50
+ <svg
51
+ width="13"
52
+ height="13"
53
+ viewBox="0 0 16 16"
54
+ fill="none"
55
+ aria-hidden="true"
56
+ >
57
+ <path
58
+ d="M3 8.5l3 3 7-7"
59
+ stroke="currentColor"
60
+ strokeWidth="1.8"
61
+ strokeLinecap="round"
62
+ strokeLinejoin="round"
63
+ />
64
+ </svg>
65
+ );
66
+ }
67
+
68
+ function XIcon() {
69
+ return (
70
+ <svg width="13" height="13" viewBox="0 0 16 16" aria-hidden="true">
71
+ <path
72
+ d="M4 4l8 8M12 4l-8 8"
73
+ stroke="currentColor"
74
+ strokeWidth="1.8"
75
+ strokeLinecap="round"
76
+ />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Click-to-edit a value in place — a static display with a pencil affordance
83
+ * that swaps to a field with explicit ✓ Save / ✗ Cancel buttons. Use it for
84
+ * values where committing an empty/invalid value would be visibly broken
85
+ * elsewhere; everything else in a modal should save as you type instead.
86
+ *
87
+ * **Escape is two-stage**: the first Esc cancels the edit; the next Esc
88
+ * closes the modal. The host coordinates this via the SDK's
89
+ * `setActiveInlineEditCancel` hook — do not add your own Escape handler.
90
+ *
91
+ * Styled purely via host-provided `.silo-inline-edit*` classes — no
92
+ * stylesheet import is needed in the extension.
93
+ *
94
+ * @example
95
+ * ```tsx
96
+ * <InlineEdit
97
+ * value={ws.name}
98
+ * onSave={(name) => ctx.workspaces.rename(ws.id, name)}
99
+ * validate={validateWorkspaceName}
100
+ * aria-label="Rename workspace"
101
+ * />
102
+ *
103
+ * <InlineEdit
104
+ * multiline
105
+ * value={description}
106
+ * onSave={setDescription}
107
+ * aria-label="Edit description"
108
+ * />
109
+ * ```
110
+ *
111
+ * @category Consumer Services
112
+ * @public
113
+ */
114
+ export function InlineEdit({
115
+ value,
116
+ onSave,
117
+ validate,
118
+ multiline = false,
119
+ "aria-label": ariaLabel,
120
+ }: {
121
+ /** The committed value. */
122
+ value: string;
123
+ /**
124
+ * Called with the trimmed, validated value; not called if unchanged.
125
+ */
126
+ onSave: (value: string) => void;
127
+ /**
128
+ * Optional — failure shows the error inline and blocks the save.
129
+ */
130
+ validate?: (v: string) => InlineEditValidation;
131
+ /** Textarea-based editing. */
132
+ multiline?: boolean;
133
+ "aria-label": string;
134
+ }): ReactNode {
135
+ // null = not editing; a string = the staged edit-mode value.
136
+ const [draft, setDraft] = useState<string | null>(null);
137
+ const [error, setError] = useState<string | null>(null);
138
+ const inputRef = useRef<HTMLInputElement | null>(null);
139
+ const textareaRef = useRef<HTMLTextAreaElement | null>(null);
140
+ const editing = draft !== null;
141
+
142
+ function cancel() {
143
+ setDraft(null);
144
+ setError(null);
145
+ }
146
+
147
+ function save() {
148
+ const raw = draft ?? "";
149
+ const result = validate
150
+ ? validate(raw)
151
+ : { ok: true as const, value: raw.trim() };
152
+ if (!result.ok) {
153
+ setError(result.error);
154
+ return;
155
+ }
156
+ if (result.value !== value) onSave(result.value);
157
+ setDraft(null);
158
+ setError(null);
159
+ }
160
+
161
+ useLayoutEffect(() => {
162
+ if (!editing) return;
163
+ const el = multiline ? textareaRef.current : inputRef.current;
164
+ if (!el) return;
165
+ el.focus();
166
+ el.select();
167
+ }, [editing, multiline]);
168
+
169
+ // Register with the host's two-stage Escape yield while editing.
170
+ useEffect(() => {
171
+ if (!editing) return;
172
+ setActiveInlineEditCancel(cancel);
173
+ return () => setActiveInlineEditCancel(null);
174
+ }, [editing]);
175
+
176
+ function onFieldKeyDown(
177
+ e: KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>,
178
+ ) {
179
+ if (multiline) {
180
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
181
+ e.preventDefault();
182
+ save();
183
+ }
184
+ return;
185
+ }
186
+ if (e.key === "Enter") {
187
+ e.preventDefault();
188
+ save();
189
+ }
190
+ }
191
+
192
+ if (!editing) {
193
+ return (
194
+ <div className="silo-inline-edit">
195
+ <div className={inlineEditDisplayClass(multiline)}>
196
+ <span className="silo-inline-edit-value">{value}</span>
197
+ <Tooltip content="Edit">
198
+ <IconButton
199
+ size="sm"
200
+ aria-label={ariaLabel}
201
+ onClick={() => {
202
+ setDraft(value);
203
+ setError(null);
204
+ }}
205
+ >
206
+ <PencilIcon />
207
+ </IconButton>
208
+ </Tooltip>
209
+ </div>
210
+ </div>
211
+ );
212
+ }
213
+
214
+ return (
215
+ <div className="silo-inline-edit">
216
+ <div className={inlineEditRowClass(multiline)}>
217
+ {multiline ? (
218
+ <Textarea
219
+ ref={textareaRef}
220
+ value={draft ?? ""}
221
+ aria-label={ariaLabel}
222
+ onChange={(e) => {
223
+ setDraft(e.target.value);
224
+ if (error) setError(null);
225
+ }}
226
+ onKeyDown={onFieldKeyDown}
227
+ autoCapitalize="off"
228
+ autoCorrect="off"
229
+ spellCheck={false}
230
+ />
231
+ ) : (
232
+ <Input
233
+ ref={inputRef}
234
+ value={draft ?? ""}
235
+ aria-label={ariaLabel}
236
+ onChange={(e) => {
237
+ setDraft(e.target.value);
238
+ if (error) setError(null);
239
+ }}
240
+ onKeyDown={onFieldKeyDown}
241
+ autoCapitalize="off"
242
+ autoCorrect="off"
243
+ spellCheck={false}
244
+ />
245
+ )}
246
+ <Tooltip content="Save">
247
+ <IconButton size="sm" aria-label="Save" onClick={save}>
248
+ <CheckIcon />
249
+ </IconButton>
250
+ </Tooltip>
251
+ <Tooltip content="Cancel">
252
+ <IconButton size="sm" aria-label="Cancel" onClick={cancel}>
253
+ <XIcon />
254
+ </IconButton>
255
+ </Tooltip>
256
+ </div>
257
+ {error && <span className="silo-inline-edit-error">{error}</span>}
258
+ </div>
259
+ );
260
+ }
package/src/Input.tsx ADDED
@@ -0,0 +1,39 @@
1
+ import { forwardRef, type InputHTMLAttributes } from "react";
2
+ import { inputClass } from "./input-classes";
3
+
4
+ /**
5
+ * The single text-input treatment used across modal content: input-bg,
6
+ * 6×8px padding, small radius, the shared focus ring.
7
+ *
8
+ * Styled purely via host-provided `.silo-input*` classes — no stylesheet
9
+ * import is needed in the extension.
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * <Input
14
+ * value={name}
15
+ * onChange={(e) => setName(e.target.value)}
16
+ * placeholder="Workspace name"
17
+ * />
18
+ * <Input block />
19
+ * ```
20
+ *
21
+ * @category Consumer Services
22
+ * @public
23
+ */
24
+ export const Input = forwardRef<
25
+ HTMLInputElement,
26
+ {
27
+ /** Stretch to the container width. */
28
+ block?: boolean;
29
+ } & InputHTMLAttributes<HTMLInputElement>
30
+ >(function Input({ block = false, className, ...rest }, ref) {
31
+ const classes = inputClass(block);
32
+ return (
33
+ <input
34
+ ref={ref}
35
+ className={className ? `${classes} ${className}` : classes}
36
+ {...rest}
37
+ />
38
+ );
39
+ });