@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.
Files changed (186) 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/editor-service.d.ts +9 -3
  90. package/dist/editor-service.d.ts.map +1 -1
  91. package/dist/empty-state-classes.d.ts +12 -0
  92. package/dist/empty-state-classes.d.ts.map +1 -0
  93. package/dist/empty-state-classes.js +8 -0
  94. package/dist/empty-state-classes.js.map +1 -0
  95. package/dist/icon-button-classes.d.ts +15 -0
  96. package/dist/icon-button-classes.d.ts.map +1 -0
  97. package/dist/icon-button-classes.js +12 -0
  98. package/dist/icon-button-classes.js.map +1 -0
  99. package/dist/index.d.ts +33 -4
  100. package/dist/index.d.ts.map +1 -1
  101. package/dist/index.js +31 -4
  102. package/dist/index.js.map +1 -1
  103. package/dist/inline-edit-classes.d.ts +12 -0
  104. package/dist/inline-edit-classes.d.ts.map +1 -0
  105. package/dist/inline-edit-classes.js +18 -0
  106. package/dist/inline-edit-classes.js.map +1 -0
  107. package/dist/inline-edit-controller.d.ts +20 -0
  108. package/dist/inline-edit-controller.d.ts.map +1 -0
  109. package/dist/inline-edit-controller.js +52 -0
  110. package/dist/inline-edit-controller.js.map +1 -0
  111. package/dist/input-classes.d.ts +8 -0
  112. package/dist/input-classes.d.ts.map +1 -0
  113. package/dist/input-classes.js +10 -0
  114. package/dist/input-classes.js.map +1 -0
  115. package/dist/list-classes.d.ts +19 -0
  116. package/dist/list-classes.d.ts.map +1 -0
  117. package/dist/list-classes.js +16 -0
  118. package/dist/list-classes.js.map +1 -0
  119. package/dist/radio-card-classes.d.ts +8 -0
  120. package/dist/radio-card-classes.d.ts.map +1 -0
  121. package/dist/radio-card-classes.js +10 -0
  122. package/dist/radio-card-classes.js.map +1 -0
  123. package/dist/search-input-classes.d.ts +8 -0
  124. package/dist/search-input-classes.d.ts.map +1 -0
  125. package/dist/search-input-classes.js +10 -0
  126. package/dist/search-input-classes.js.map +1 -0
  127. package/dist/segmented-tabs-classes.d.ts +8 -0
  128. package/dist/segmented-tabs-classes.d.ts.map +1 -0
  129. package/dist/segmented-tabs-classes.js +10 -0
  130. package/dist/segmented-tabs-classes.js.map +1 -0
  131. package/dist/switch-classes.d.ts +8 -0
  132. package/dist/switch-classes.d.ts.map +1 -0
  133. package/dist/switch-classes.js +10 -0
  134. package/dist/switch-classes.js.map +1 -0
  135. package/dist/tabs-classes.d.ts +8 -0
  136. package/dist/tabs-classes.d.ts.map +1 -0
  137. package/dist/tabs-classes.js +10 -0
  138. package/dist/tabs-classes.js.map +1 -0
  139. package/package.json +1 -1
  140. package/src/AddRow.tsx +56 -0
  141. package/src/Badge.tsx +55 -0
  142. package/src/Button.tsx +52 -0
  143. package/src/Callout.tsx +26 -0
  144. package/src/CheckboxRow.tsx +54 -0
  145. package/src/EmptyState.tsx +69 -0
  146. package/src/IconButton.tsx +55 -0
  147. package/src/InlineEdit.tsx +260 -0
  148. package/src/Input.tsx +39 -0
  149. package/src/List.tsx +240 -0
  150. package/src/ModalActions.tsx +52 -0
  151. package/src/RadioGroup.tsx +101 -0
  152. package/src/SearchInput.tsx +98 -0
  153. package/src/Section.tsx +49 -0
  154. package/src/SegmentedTabs.tsx +70 -0
  155. package/src/Select.tsx +36 -0
  156. package/src/SettingRow.tsx +47 -0
  157. package/src/Switch.tsx +63 -0
  158. package/src/Tabs.tsx +89 -0
  159. package/src/Textarea.tsx +33 -0
  160. package/src/badge-classes.test.ts +20 -0
  161. package/src/badge-classes.ts +27 -0
  162. package/src/button-classes.test.ts +24 -0
  163. package/src/button-classes.ts +32 -0
  164. package/src/editor-service.ts +9 -3
  165. package/src/empty-state-classes.test.ts +13 -0
  166. package/src/empty-state-classes.ts +17 -0
  167. package/src/icon-button-classes.test.ts +13 -0
  168. package/src/icon-button-classes.ts +19 -0
  169. package/src/index.ts +45 -4
  170. package/src/inline-edit-classes.test.ts +27 -0
  171. package/src/inline-edit-classes.ts +18 -0
  172. package/src/inline-edit-controller.ts +53 -0
  173. package/src/input-classes.test.ts +13 -0
  174. package/src/input-classes.ts +9 -0
  175. package/src/list-classes.test.ts +23 -0
  176. package/src/list-classes.ts +26 -0
  177. package/src/radio-card-classes.test.ts +12 -0
  178. package/src/radio-card-classes.ts +9 -0
  179. package/src/search-input-classes.test.ts +12 -0
  180. package/src/search-input-classes.ts +9 -0
  181. package/src/segmented-tabs-classes.test.ts +12 -0
  182. package/src/segmented-tabs-classes.ts +9 -0
  183. package/src/switch-classes.test.ts +9 -0
  184. package/src/switch-classes.ts +9 -0
  185. package/src/tabs-classes.test.ts +12 -0
  186. package/src/tabs-classes.ts +9 -0
package/src/Switch.tsx ADDED
@@ -0,0 +1,63 @@
1
+ import type { ButtonHTMLAttributes } from "react";
2
+ import { switchDataChecked } from "./switch-classes";
3
+
4
+ /**
5
+ * The iOS-style on/off toggle used throughout settings. Off = recessed
6
+ * `bg-active` track with a hairline border; on = accent fill. Focus uses a
7
+ * positive-offset ring (the one deliberate exception to the shared inset
8
+ * ring — an 18px pill can't take an inset outline).
9
+ *
10
+ * Styled purely via the host-provided `.silo-switch-track` class — no
11
+ * stylesheet import is needed in the extension.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * <Switch
16
+ * checked={formatOnSave}
17
+ * onChange={setFormatOnSave}
18
+ * aria-label="Format on save"
19
+ * />
20
+ * ```
21
+ *
22
+ * @category Consumer Services
23
+ * @public
24
+ */
25
+ export function Switch({
26
+ checked,
27
+ onChange,
28
+ disabled,
29
+ "aria-label": ariaLabel,
30
+ className,
31
+ type = "button",
32
+ ...rest
33
+ }: {
34
+ checked: boolean;
35
+ onChange: (checked: boolean) => void;
36
+ disabled?: boolean;
37
+ /**
38
+ * Required unless wrapped by a labeled {@link SettingRow}.
39
+ */
40
+ "aria-label"?: string;
41
+ } & Omit<
42
+ ButtonHTMLAttributes<HTMLButtonElement>,
43
+ "onChange" | "aria-label" | "children" | "role" | "aria-checked"
44
+ >) {
45
+ return (
46
+ <button
47
+ type={type}
48
+ role="switch"
49
+ aria-checked={checked}
50
+ aria-label={ariaLabel}
51
+ disabled={disabled}
52
+ data-checked={switchDataChecked(checked)}
53
+ className={
54
+ className ? `silo-switch-track ${className}` : "silo-switch-track"
55
+ }
56
+ onClick={() => {
57
+ if (disabled) return;
58
+ onChange(!checked);
59
+ }}
60
+ {...rest}
61
+ />
62
+ );
63
+ }
package/src/Tabs.tsx ADDED
@@ -0,0 +1,89 @@
1
+ import type { ReactNode } from "react";
2
+ import { tabDataActive } from "./tabs-classes";
3
+
4
+ /**
5
+ * One tab in a {@link Tabs} strip.
6
+ *
7
+ * @category Core Types
8
+ * @public
9
+ */
10
+ export interface TabItem<T extends string = string> {
11
+ id: T;
12
+ label: string;
13
+ icon?: ReactNode;
14
+ }
15
+
16
+ /**
17
+ * A borderless strip attached to the content panel it switches. The active
18
+ * tab's background **is** the panel's background — that shared fill is what
19
+ * makes the tab read as part of the panel. Pair with {@link TabPanel}.
20
+ * Native tab stops per tab; no arrow-key roving.
21
+ *
22
+ * Styled purely via host-provided `.silo-tabs` / `.silo-tab` classes — no
23
+ * stylesheet import is needed in the extension.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * <Tabs
28
+ * tabs={[
29
+ * { id: "panels", label: "Side Panels" },
30
+ * { id: "statusBar", label: "Status Bar" },
31
+ * { id: "options", label: "Options" },
32
+ * ]}
33
+ * active={tab}
34
+ * onSelect={setTab}
35
+ * />
36
+ * <TabPanel>{tab === "panels" && <PanelsSettings />}</TabPanel>
37
+ * ```
38
+ *
39
+ * @category Consumer Services
40
+ * @public
41
+ */
42
+ export function Tabs<T extends string>({
43
+ tabs,
44
+ active,
45
+ onSelect,
46
+ }: {
47
+ tabs: TabItem<T>[];
48
+ active: T;
49
+ onSelect: (id: T) => void;
50
+ }) {
51
+ return (
52
+ <div className="silo-tabs" role="tablist">
53
+ {tabs.map((tab) => {
54
+ const isActive = tab.id === active;
55
+ const dataActive = tabDataActive(isActive);
56
+ return (
57
+ <button
58
+ key={tab.id}
59
+ type="button"
60
+ role="tab"
61
+ aria-selected={isActive}
62
+ className="silo-tab"
63
+ {...(dataActive != null ? { "data-active": dataActive } : {})}
64
+ onClick={() => onSelect(tab.id)}
65
+ >
66
+ {tab.icon}
67
+ {tab.label}
68
+ </button>
69
+ );
70
+ })}
71
+ </div>
72
+ );
73
+ }
74
+
75
+ /**
76
+ * The content panel a {@link Tabs} strip attaches to. Active-tab and panel
77
+ * share the same background token — that's the "attached" illusion.
78
+ *
79
+ * @example
80
+ * ```tsx
81
+ * <TabPanel>{tab === "panels" && <PanelsSettings />}</TabPanel>
82
+ * ```
83
+ *
84
+ * @category Consumer Services
85
+ * @public
86
+ */
87
+ export function TabPanel({ children }: { children?: ReactNode }) {
88
+ return <div className="silo-tab-panel">{children}</div>;
89
+ }
@@ -0,0 +1,33 @@
1
+ import { forwardRef, type TextareaHTMLAttributes } from "react";
2
+
3
+ /**
4
+ * Multi-line text entry wearing the same tokens as {@link Input}, plus
5
+ * comfortable line-height, vertical resize, and a 64px minimum height.
6
+ *
7
+ * Styled purely via the host-provided `.silo-textarea` class — no
8
+ * stylesheet import is needed in the extension.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * <Textarea
13
+ * value={notes}
14
+ * onChange={(e) => setNotes(e.target.value)}
15
+ * placeholder="Notes…"
16
+ * />
17
+ * ```
18
+ *
19
+ * @category Consumer Services
20
+ * @public
21
+ */
22
+ export const Textarea = forwardRef<
23
+ HTMLTextAreaElement,
24
+ TextareaHTMLAttributes<HTMLTextAreaElement>
25
+ >(function Textarea({ className, ...rest }, ref) {
26
+ return (
27
+ <textarea
28
+ ref={ref}
29
+ className={className ? `silo-textarea ${className}` : "silo-textarea"}
30
+ {...rest}
31
+ />
32
+ );
33
+ });
@@ -0,0 +1,20 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { badgeClass } from "./badge-classes";
3
+
4
+ describe("badgeClass", () => {
5
+ it("defaults to neutral", () => {
6
+ expect(badgeClass()).toBe("silo-badge silo-badge-neutral");
7
+ });
8
+
9
+ it("maps every tone to silo-badge-{tone}", () => {
10
+ expect(badgeClass("accent")).toBe("silo-badge silo-badge-accent");
11
+ expect(badgeClass("ok")).toBe("silo-badge silo-badge-ok");
12
+ expect(badgeClass("warn")).toBe("silo-badge silo-badge-warn");
13
+ expect(badgeClass("err")).toBe("silo-badge silo-badge-err");
14
+ expect(badgeClass("outline")).toBe("silo-badge silo-badge-outline");
15
+ });
16
+
17
+ it("uses silo-badge-custom when color is set (overrides tone)", () => {
18
+ expect(badgeClass("ok", "#e06c75")).toBe("silo-badge silo-badge-custom");
19
+ });
20
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Class-string mapping for {@link Badge} — pinned by unit tests so a rename
3
+ * of the host contract classes fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export type BadgeTone =
8
+ | "neutral"
9
+ | "accent"
10
+ | "ok"
11
+ | "warn"
12
+ | "err"
13
+ | "outline";
14
+
15
+ /**
16
+ * Resolve the host `.silo-badge*` class string. An arbitrary `color` wins over
17
+ * `tone` and selects `.silo-badge-custom` (fed via `--badge-color`).
18
+ *
19
+ * @internal
20
+ */
21
+ export function badgeClass(
22
+ tone: BadgeTone = "neutral",
23
+ color?: string,
24
+ ): string {
25
+ if (color != null) return "silo-badge silo-badge-custom";
26
+ return `silo-badge silo-badge-${tone}`;
27
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buttonClass } from "./button-classes";
3
+
4
+ describe("buttonClass", () => {
5
+ it("maps normal variant to silo-button", () => {
6
+ expect(buttonClass("normal")).toBe("silo-button");
7
+ expect(buttonClass()).toBe("silo-button");
8
+ });
9
+
10
+ it("maps primary and danger to their standalone classes", () => {
11
+ expect(buttonClass("primary")).toBe("silo-button-primary");
12
+ expect(buttonClass("danger")).toBe("silo-button-danger");
13
+ });
14
+
15
+ it("composes silo-button-sm onto any variant", () => {
16
+ expect(buttonClass("normal", "sm")).toBe("silo-button silo-button-sm");
17
+ expect(buttonClass("primary", "sm")).toBe(
18
+ "silo-button-primary silo-button-sm",
19
+ );
20
+ expect(buttonClass("danger", "sm")).toBe(
21
+ "silo-button-danger silo-button-sm",
22
+ );
23
+ });
24
+ });
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Class-string mapping for {@link Button} — pinned by unit tests so a rename
3
+ * of the host contract classes fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export type ButtonVariant = "normal" | "primary" | "danger";
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export type ButtonSize = "normal" | "sm";
13
+
14
+ /**
15
+ * Resolve the host `.silo-button*` class string for a Button's `variant` +
16
+ * `size`. Primary/danger are standalone class names (not modifiers on
17
+ * `.silo-button`); `sm` composes with any variant.
18
+ *
19
+ * @internal
20
+ */
21
+ export function buttonClass(
22
+ variant: ButtonVariant = "normal",
23
+ size: ButtonSize = "normal",
24
+ ): string {
25
+ const base =
26
+ variant === "primary"
27
+ ? "silo-button-primary"
28
+ : variant === "danger"
29
+ ? "silo-button-danger"
30
+ : "silo-button";
31
+ return size === "sm" ? `${base} silo-button-sm` : base;
32
+ }
@@ -118,8 +118,10 @@ export interface DiffContent {
118
118
 
119
119
  /**
120
120
  * The request a {@link DiffContentProvider} receives to resolve a diff's two
121
- * sides — the {@link OpenDiffSpec}'s `filePath`/`args` plus the folder of the
122
- * workspace the diff lives in (the natural cwd for path-relative providers).
121
+ * sides — the {@link OpenDiffSpec}'s `filePath`/`args` plus the workspace
122
+ * folder that contains that file (the natural cwd for path-relative providers).
123
+ * In a multi-root workspace this is the matching root (primary or an
124
+ * `extraFolder` such as an opened git worktree), not always the primary folder.
123
125
  *
124
126
  * @category Consumer Services
125
127
  * @public
@@ -129,7 +131,11 @@ export interface DiffContentRequest {
129
131
  filePath: string;
130
132
  /** The provider's args (from {@link OpenDiffSpec.args}). */
131
133
  args?: Record<string, unknown>;
132
- /** Folder of the workspace the diff lives in, or `null` if none. */
134
+ /**
135
+ * Workspace root that contains {@link DiffContentRequest.filePath}, or
136
+ * `null` if none. Prefer this over the workspace primary folder when roots
137
+ * differ (e.g. a linked worktree opened alongside).
138
+ */
133
139
  workspaceFolder: string | null;
134
140
  }
135
141
 
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { emptyStateIconDataTone } from "./empty-state-classes";
3
+
4
+ describe("emptyStateIconDataTone", () => {
5
+ it("omits data-tone for the neutral default", () => {
6
+ expect(emptyStateIconDataTone()).toBeUndefined();
7
+ expect(emptyStateIconDataTone("neutral")).toBeUndefined();
8
+ });
9
+
10
+ it("sets data-tone=ok for the positive-empty tone", () => {
11
+ expect(emptyStateIconDataTone("ok")).toBe("ok");
12
+ });
13
+ });
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Class-string / data-attribute mapping for {@link EmptyState} — pinned by
3
+ * unit tests so a rename of the host contract fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export type EmptyStateTone = "ok" | "neutral";
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export function emptyStateIconDataTone(
13
+ tone: EmptyStateTone = "neutral",
14
+ ): "ok" | undefined {
15
+ // CSS keys the ok tone on data-tone="ok"; neutral is the default (no attr).
16
+ return tone === "ok" ? "ok" : undefined;
17
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { iconButtonClass } from "./icon-button-classes";
3
+
4
+ describe("iconButtonClass", () => {
5
+ it("defaults to silo-icon-button", () => {
6
+ expect(iconButtonClass()).toBe("silo-icon-button");
7
+ expect(iconButtonClass("normal")).toBe("silo-icon-button");
8
+ });
9
+
10
+ it("adds the sm modifier", () => {
11
+ expect(iconButtonClass("sm")).toBe("silo-icon-button silo-icon-button-sm");
12
+ });
13
+ });
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Class-string mapping for {@link IconButton} — pinned by unit tests so a
3
+ * rename of the host contract classes fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export type IconButtonSize = "normal" | "sm";
8
+
9
+ /**
10
+ * Resolve the host `.silo-icon-button*` class string for an IconButton's
11
+ * `size`. Default is the 32px standalone size; `sm` adds the 26px modifier.
12
+ *
13
+ * @internal
14
+ */
15
+ export function iconButtonClass(size: IconButtonSize = "normal"): string {
16
+ return size === "sm"
17
+ ? "silo-icon-button silo-icon-button-sm"
18
+ : "silo-icon-button";
19
+ }
package/src/index.ts CHANGED
@@ -5,10 +5,10 @@
5
5
  * host-internal and may change without notice.
6
6
  *
7
7
  * **What's here:** the types-first extension contract (see `types.ts`) plus a
8
- * small set of blessed runtime helpers (`Tooltip`, `useFocusGroup`,
9
- * `useServiceState`, `DND_MIME`, `PathDeniedError`, `NetworkError`). The SDK
10
- * peer-depends on React 19; changes to the runtime helpers can be breaking
11
- * even when the types are unchanged.
8
+ * small set of blessed runtime helpers (`Tooltip`, the modal design-system
9
+ * kit, `useFocusGroup`, `useServiceState`, `DND_MIME`, `PathDeniedError`,
10
+ * `NetworkError`). The SDK peer-depends on React 19; changes to the runtime
11
+ * helpers can be breaking even when the types are unchanged.
12
12
  *
13
13
  * This is also the entry point the API-reference generator (TypeDoc) reads, so
14
14
  * the published reference is exactly this surface — no more, no less.
@@ -220,6 +220,39 @@ export { path } from "./path";
220
220
  // Extensions use this instead of native `title` attributes to match host chrome.
221
221
  export { Tooltip } from "./Tooltip";
222
222
 
223
+ // Modal design-system kit (RFC 0016 / ADR 0026) — presentational components
224
+ // styled purely via host-provided `.silo-*` classes.
225
+ export { Button } from "./Button";
226
+ export type { ButtonVariant, ButtonSize } from "./Button";
227
+ export { IconButton } from "./IconButton";
228
+ export type { IconButtonSize } from "./IconButton";
229
+ export { Input } from "./Input";
230
+ export { Textarea } from "./Textarea";
231
+ export { SearchInput } from "./SearchInput";
232
+ export { InlineEdit } from "./InlineEdit";
233
+ export type { InlineEditValidation } from "./InlineEdit";
234
+ export { Select } from "./Select";
235
+ export { Switch } from "./Switch";
236
+ export { CheckboxRow } from "./CheckboxRow";
237
+ export { RadioGroup, RadioCard } from "./RadioGroup";
238
+ export { ModalActions } from "./ModalActions";
239
+
240
+ // Structure half of the modal design-system kit (RFC 0016).
241
+ export { SegmentedTabs } from "./SegmentedTabs";
242
+ export type { SegmentedTabItem } from "./SegmentedTabs";
243
+ export { Tabs, TabPanel } from "./Tabs";
244
+ export type { TabItem } from "./Tabs";
245
+ export { List, ListRow } from "./List";
246
+ export type { ListRowProps, ListRowTruncate } from "./List";
247
+ export { AddRow } from "./AddRow";
248
+ export { Badge } from "./Badge";
249
+ export type { BadgeTone } from "./Badge";
250
+ export { EmptyState } from "./EmptyState";
251
+ export type { EmptyStateTone } from "./EmptyState";
252
+ export { Callout } from "./Callout";
253
+ export { Section } from "./Section";
254
+ export { SettingRow } from "./SettingRow";
255
+
223
256
  // Runtime helpers. The one blessed way for an extension to read a `ctx`
224
257
  // service's reactive state in React — replaces hand-rolled useSyncExternalStore.
225
258
  export { useServiceState } from "./use-service-state";
@@ -237,3 +270,11 @@ export type {
237
270
  FocusGroupOrientation,
238
271
  FocusGroupNavQuery,
239
272
  } from "./use-focus-group";
273
+
274
+ // Host/InlineEdit coordination for InlineEdit's two-stage Escape (RFC 0016).
275
+ // @internal — host-only plumbing, not part of the documented reference;
276
+ // extension authors never call these directly.
277
+ export {
278
+ setActiveInlineEditCancel,
279
+ yieldEscapeToInlineEdit,
280
+ } from "./inline-edit-controller";
@@ -0,0 +1,27 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ inlineEditDisplayClass,
4
+ inlineEditRowClass,
5
+ } from "./inline-edit-classes";
6
+
7
+ describe("inlineEditDisplayClass", () => {
8
+ it("is silo-inline-edit-display by default", () => {
9
+ expect(inlineEditDisplayClass(false)).toBe("silo-inline-edit-display");
10
+ });
11
+
12
+ it("adds multiline", () => {
13
+ expect(inlineEditDisplayClass(true)).toBe(
14
+ "silo-inline-edit-display multiline",
15
+ );
16
+ });
17
+ });
18
+
19
+ describe("inlineEditRowClass", () => {
20
+ it("is silo-inline-edit-row by default", () => {
21
+ expect(inlineEditRowClass(false)).toBe("silo-inline-edit-row");
22
+ });
23
+
24
+ it("adds multiline", () => {
25
+ expect(inlineEditRowClass(true)).toBe("silo-inline-edit-row multiline");
26
+ });
27
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Class-string mapping for {@link InlineEdit} — pinned by unit tests so a
3
+ * rename of the host contract classes fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export function inlineEditDisplayClass(multiline: boolean): string {
8
+ return multiline
9
+ ? "silo-inline-edit-display multiline"
10
+ : "silo-inline-edit-display";
11
+ }
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export function inlineEditRowClass(multiline: boolean): string {
17
+ return multiline ? "silo-inline-edit-row multiline" : "silo-inline-edit-row";
18
+ }
@@ -0,0 +1,53 @@
1
+ // Coordination for `InlineEdit`'s two-stage Escape (RFC 0016): the first
2
+ // Escape cancels an in-progress edit, the next Escape closes the enclosing
3
+ // modal. Mirrors the host's `menu-controller.ts` `getMenu()` pattern — a
4
+ // single piece of module state the modal's own Escape handler checks before
5
+ // acting on the key itself — because `InlineEdit` doesn't have (and doesn't
6
+ // need) a document-level keydown listener of its own; the host's `Modal.tsx`
7
+ // already owns one.
8
+ //
9
+ // Lives in the SDK, not the host, even though only host code (`Modal.tsx`)
10
+ // calls `yieldEscapeToInlineEdit`: the dependency only runs one direction
11
+ // (extension-host depends on @silo-code/sdk, never the reverse), and
12
+ // `InlineEdit` — a public SDK component — is the thing calling
13
+ // `setActiveInlineEditCancel`. Exported from the barrel so the host can
14
+ // import it like anything else in `@silo-code/sdk`, tagged `@internal` to
15
+ // keep it out of the public docs reference — extension authors never call
16
+ // these directly.
17
+ //
18
+ // This intentionally works regardless of the modal's `dismissible` option:
19
+ // InlineEdit ships on `Workspace Properties`-style non-dismissible modals
20
+ // (protecting other staged input) as often as dismissible ones, and
21
+ // cancelling an edit-in-place is a narrower, always-safe action than closing
22
+ // the whole modal.
23
+
24
+ let activeCancel: (() => void) | null = null;
25
+
26
+ /**
27
+ * Registers the currently-editing `InlineEdit`'s cancel callback so the
28
+ * enclosing modal's Escape handler can yield to it. Call with `null` when
29
+ * editing stops for any reason (save, the ✗ button, unmount) so a stale
30
+ * callback never lingers.
31
+ *
32
+ * @category Consumer Services
33
+ * @internal
34
+ */
35
+ export function setActiveInlineEditCancel(cancel: (() => void) | null): void {
36
+ activeCancel = cancel;
37
+ }
38
+
39
+ /**
40
+ * If an `InlineEdit` is mid-edit, cancels it and returns `true` (the
41
+ * caller's Escape handler should stop there, not close the modal). Returns
42
+ * `false` when nothing is being edited.
43
+ *
44
+ * @category Consumer Services
45
+ * @internal
46
+ */
47
+ export function yieldEscapeToInlineEdit(): boolean {
48
+ if (!activeCancel) return false;
49
+ const cancel = activeCancel;
50
+ activeCancel = null;
51
+ cancel();
52
+ return true;
53
+ }
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { inputClass } from "./input-classes";
3
+
4
+ describe("inputClass", () => {
5
+ it("defaults to silo-input", () => {
6
+ expect(inputClass()).toBe("silo-input");
7
+ expect(inputClass(false)).toBe("silo-input");
8
+ });
9
+
10
+ it("adds silo-input-block when block is true", () => {
11
+ expect(inputClass(true)).toBe("silo-input silo-input-block");
12
+ });
13
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Class-string mapping for {@link Input} — pinned by unit tests so a rename
3
+ * of the host contract classes fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export function inputClass(block = false): string {
8
+ return block ? "silo-input silo-input-block" : "silo-input";
9
+ }
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { listRowDataSelected, listRowNameTruncate } from "./list-classes";
3
+
4
+ describe("listRowDataSelected", () => {
5
+ it("sets data-selected=true when selected", () => {
6
+ expect(listRowDataSelected(true)).toBe("true");
7
+ });
8
+
9
+ it("omits the attribute when not selected", () => {
10
+ expect(listRowDataSelected(false)).toBeUndefined();
11
+ });
12
+ });
13
+
14
+ describe("listRowNameTruncate", () => {
15
+ it("defaults to end truncation (no data attribute)", () => {
16
+ expect(listRowNameTruncate()).toBeUndefined();
17
+ expect(listRowNameTruncate("end")).toBeUndefined();
18
+ });
19
+
20
+ it("sets data-truncate=start for front truncation", () => {
21
+ expect(listRowNameTruncate("start")).toBe("start");
22
+ });
23
+ });
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Class-string / data-attribute mapping for {@link ListRow} — pinned by unit
3
+ * tests so a rename of the host contract fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export type ListRowTruncate = "end" | "start";
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export function listRowDataSelected(selected: boolean): "true" | undefined {
13
+ return selected ? "true" : undefined;
14
+ }
15
+
16
+ /**
17
+ * Front-truncation is expressed as `data-truncate="start"` on
18
+ * `.silo-list-row-name`; end-truncation is the CSS default (no attribute).
19
+ *
20
+ * @internal
21
+ */
22
+ export function listRowNameTruncate(
23
+ truncate: ListRowTruncate = "end",
24
+ ): "start" | undefined {
25
+ return truncate === "start" ? "start" : undefined;
26
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { radioCardDataSelected } from "./radio-card-classes";
3
+
4
+ describe("radioCardDataSelected", () => {
5
+ it("sets data-selected=true when selected", () => {
6
+ expect(radioCardDataSelected(true)).toBe("true");
7
+ });
8
+
9
+ it("omits the attribute when not selected", () => {
10
+ expect(radioCardDataSelected(false)).toBeUndefined();
11
+ });
12
+ });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `data-selected` value for {@link RadioCard} — pinned by unit tests so a
3
+ * rename of the host contract attribute fails CI (RFC 0016).
4
+ *
5
+ * @internal
6
+ */
7
+ export function radioCardDataSelected(selected: boolean): "true" | undefined {
8
+ return selected ? "true" : undefined;
9
+ }
@@ -0,0 +1,12 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { searchInputClass } from "./search-input-classes";
3
+
4
+ describe("searchInputClass", () => {
5
+ it("is silo-search-input when empty", () => {
6
+ expect(searchInputClass(false)).toBe("silo-search-input");
7
+ });
8
+
9
+ it("adds has-value when there is a value", () => {
10
+ expect(searchInputClass(true)).toBe("silo-search-input has-value");
11
+ });
12
+ });