@silo-code/sdk 0.28.1 → 0.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +8 -4
- 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 +8 -4
- 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/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
|
+
}
|
package/src/Textarea.tsx
ADDED
|
@@ -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
|
+
}
|
package/src/editor-service.ts
CHANGED
|
@@ -291,9 +291,12 @@ export interface EditorService {
|
|
|
291
291
|
* The current buffer text of an open editor tab, including unsaved edits.
|
|
292
292
|
*
|
|
293
293
|
* Resolves `undefined` when the tab isn't text-backed (e.g. the image
|
|
294
|
-
* viewer)
|
|
295
|
-
*
|
|
296
|
-
*
|
|
294
|
+
* viewer), has never mounted a text document, and has no retained unsaved
|
|
295
|
+
* buffer. A dirty text editor that unmounts on a view switch (e.g. Text →
|
|
296
|
+
* Markdown Preview) retains its buffer so presenters can still read it. A
|
|
297
|
+
* lazy-mounted dock panel is **not** force-mounted to read its text. It is
|
|
298
|
+
* async precisely because the live text lives in the mounted editor
|
|
299
|
+
* component, not in host state.
|
|
297
300
|
*
|
|
298
301
|
* @example
|
|
299
302
|
* ```ts
|
|
@@ -304,7 +307,8 @@ export interface EditorService {
|
|
|
304
307
|
getText(editorId: string): Promise<string | undefined>;
|
|
305
308
|
/**
|
|
306
309
|
* Whether an open editor tab has unsaved changes. Returns `false` for an
|
|
307
|
-
* unknown id or a tab that isn't
|
|
310
|
+
* unknown id or a tab that isn't text-backed and has no retained dirty
|
|
311
|
+
* buffer. Stays `true` across a dirty Text → Preview view switch.
|
|
308
312
|
*/
|
|
309
313
|
isDirty(editorId: string): boolean;
|
|
310
314
|
/**
|
|
@@ -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`,
|
|
9
|
-
* `useServiceState`, `DND_MIME`, `PathDeniedError`,
|
|
10
|
-
* peer-depends on React 19; changes to the runtime
|
|
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
|
+
});
|