@silo-code/sdk 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AddRow.d.ts +21 -0
- package/dist/AddRow.d.ts.map +1 -0
- package/dist/AddRow.js +24 -0
- package/dist/AddRow.js.map +1 -0
- package/dist/Badge.d.ts +32 -0
- package/dist/Badge.d.ts.map +1 -0
- package/dist/Badge.js +32 -0
- package/dist/Badge.js.map +1 -0
- package/dist/Button.d.ts +29 -0
- package/dist/Button.d.ts.map +1 -0
- package/dist/Button.js +25 -0
- package/dist/Button.js.map +1 -0
- package/dist/Callout.d.ts +26 -0
- package/dist/Callout.d.ts.map +1 -0
- package/dist/Callout.js +26 -0
- package/dist/Callout.js.map +1 -0
- package/dist/CheckboxRow.d.ts +27 -0
- package/dist/CheckboxRow.d.ts.map +1 -0
- package/dist/CheckboxRow.js +24 -0
- package/dist/CheckboxRow.js.map +1 -0
- package/dist/EmptyState.d.ts +40 -0
- package/dist/EmptyState.d.ts.map +1 -0
- package/dist/EmptyState.js +34 -0
- package/dist/EmptyState.js.map +1 -0
- package/dist/IconButton.d.ts +35 -0
- package/dist/IconButton.d.ts.map +1 -0
- package/dist/IconButton.js +31 -0
- package/dist/IconButton.js.map +1 -0
- package/dist/InlineEdit.d.ts +63 -0
- package/dist/InlineEdit.d.ts.map +1 -0
- package/dist/InlineEdit.js +121 -0
- package/dist/InlineEdit.js.map +1 -0
- package/dist/Input.d.ts +26 -0
- package/dist/Input.d.ts.map +1 -0
- package/dist/Input.js +28 -0
- package/dist/Input.js.map +1 -0
- package/dist/List.d.ts +74 -0
- package/dist/List.d.ts.map +1 -0
- package/dist/List.js +104 -0
- package/dist/List.js.map +1 -0
- package/dist/ModalActions.d.ts +40 -0
- package/dist/ModalActions.d.ts.map +1 -0
- package/dist/ModalActions.js +36 -0
- package/dist/ModalActions.js.map +1 -0
- package/dist/RadioGroup.d.ts +55 -0
- package/dist/RadioGroup.d.ts.map +1 -0
- package/dist/RadioGroup.js +60 -0
- package/dist/RadioGroup.js.map +1 -0
- package/dist/SearchInput.d.ts +30 -0
- package/dist/SearchInput.d.ts.map +1 -0
- package/dist/SearchInput.js +36 -0
- package/dist/SearchInput.js.map +1 -0
- package/dist/Section.d.ts +35 -0
- package/dist/Section.d.ts.map +1 -0
- package/dist/Section.js +31 -0
- package/dist/Section.js.map +1 -0
- package/dist/SegmentedTabs.d.ts +41 -0
- package/dist/SegmentedTabs.d.ts.map +1 -0
- package/dist/SegmentedTabs.js +33 -0
- package/dist/SegmentedTabs.js.map +1 -0
- package/dist/Select.d.ts +23 -0
- package/dist/Select.d.ts.map +1 -0
- package/dist/Select.js +23 -0
- package/dist/Select.js.map +1 -0
- package/dist/SettingRow.d.ts +33 -0
- package/dist/SettingRow.d.ts.map +1 -0
- package/dist/SettingRow.js +30 -0
- package/dist/SettingRow.js.map +1 -0
- package/dist/Switch.d.ts +32 -0
- package/dist/Switch.d.ts.map +1 -0
- package/dist/Switch.js +31 -0
- package/dist/Switch.js.map +1 -0
- package/dist/Tabs.d.ts +59 -0
- package/dist/Tabs.d.ts.map +1 -0
- package/dist/Tabs.js +51 -0
- package/dist/Tabs.js.map +1 -0
- package/dist/Textarea.d.ts +22 -0
- package/dist/Textarea.d.ts.map +1 -0
- package/dist/Textarea.js +25 -0
- package/dist/Textarea.js.map +1 -0
- package/dist/badge-classes.d.ts +15 -0
- package/dist/badge-classes.d.ts.map +1 -0
- package/dist/badge-classes.js +12 -0
- package/dist/badge-classes.js.map +1 -0
- package/dist/button-classes.d.ts +20 -0
- package/dist/button-classes.d.ts.map +1 -0
- package/dist/button-classes.js +16 -0
- package/dist/button-classes.js.map +1 -0
- package/dist/editor-service.d.ts +9 -3
- package/dist/editor-service.d.ts.map +1 -1
- package/dist/empty-state-classes.d.ts +12 -0
- package/dist/empty-state-classes.d.ts.map +1 -0
- package/dist/empty-state-classes.js +8 -0
- package/dist/empty-state-classes.js.map +1 -0
- package/dist/icon-button-classes.d.ts +15 -0
- package/dist/icon-button-classes.d.ts.map +1 -0
- package/dist/icon-button-classes.js +12 -0
- package/dist/icon-button-classes.js.map +1 -0
- package/dist/index.d.ts +33 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +31 -4
- package/dist/index.js.map +1 -1
- package/dist/inline-edit-classes.d.ts +12 -0
- package/dist/inline-edit-classes.d.ts.map +1 -0
- package/dist/inline-edit-classes.js +18 -0
- package/dist/inline-edit-classes.js.map +1 -0
- package/dist/inline-edit-controller.d.ts +20 -0
- package/dist/inline-edit-controller.d.ts.map +1 -0
- package/dist/inline-edit-controller.js +52 -0
- package/dist/inline-edit-controller.js.map +1 -0
- package/dist/input-classes.d.ts +8 -0
- package/dist/input-classes.d.ts.map +1 -0
- package/dist/input-classes.js +10 -0
- package/dist/input-classes.js.map +1 -0
- package/dist/list-classes.d.ts +19 -0
- package/dist/list-classes.d.ts.map +1 -0
- package/dist/list-classes.js +16 -0
- package/dist/list-classes.js.map +1 -0
- package/dist/radio-card-classes.d.ts +8 -0
- package/dist/radio-card-classes.d.ts.map +1 -0
- package/dist/radio-card-classes.js +10 -0
- package/dist/radio-card-classes.js.map +1 -0
- package/dist/search-input-classes.d.ts +8 -0
- package/dist/search-input-classes.d.ts.map +1 -0
- package/dist/search-input-classes.js +10 -0
- package/dist/search-input-classes.js.map +1 -0
- package/dist/segmented-tabs-classes.d.ts +8 -0
- package/dist/segmented-tabs-classes.d.ts.map +1 -0
- package/dist/segmented-tabs-classes.js +10 -0
- package/dist/segmented-tabs-classes.js.map +1 -0
- package/dist/switch-classes.d.ts +8 -0
- package/dist/switch-classes.d.ts.map +1 -0
- package/dist/switch-classes.js +10 -0
- package/dist/switch-classes.js.map +1 -0
- package/dist/tabs-classes.d.ts +8 -0
- package/dist/tabs-classes.d.ts.map +1 -0
- package/dist/tabs-classes.js +10 -0
- package/dist/tabs-classes.js.map +1 -0
- package/package.json +1 -1
- package/src/AddRow.tsx +56 -0
- package/src/Badge.tsx +55 -0
- package/src/Button.tsx +52 -0
- package/src/Callout.tsx +26 -0
- package/src/CheckboxRow.tsx +54 -0
- package/src/EmptyState.tsx +69 -0
- package/src/IconButton.tsx +55 -0
- package/src/InlineEdit.tsx +260 -0
- package/src/Input.tsx +39 -0
- package/src/List.tsx +240 -0
- package/src/ModalActions.tsx +52 -0
- package/src/RadioGroup.tsx +101 -0
- package/src/SearchInput.tsx +98 -0
- package/src/Section.tsx +49 -0
- package/src/SegmentedTabs.tsx +70 -0
- package/src/Select.tsx +36 -0
- package/src/SettingRow.tsx +47 -0
- package/src/Switch.tsx +63 -0
- package/src/Tabs.tsx +89 -0
- package/src/Textarea.tsx +33 -0
- package/src/badge-classes.test.ts +20 -0
- package/src/badge-classes.ts +27 -0
- package/src/button-classes.test.ts +24 -0
- package/src/button-classes.ts +32 -0
- package/src/editor-service.ts +9 -3
- package/src/empty-state-classes.test.ts +13 -0
- package/src/empty-state-classes.ts +17 -0
- package/src/icon-button-classes.test.ts +13 -0
- package/src/icon-button-classes.ts +19 -0
- package/src/index.ts +45 -4
- package/src/inline-edit-classes.test.ts +27 -0
- package/src/inline-edit-classes.ts +18 -0
- package/src/inline-edit-controller.ts +53 -0
- package/src/input-classes.test.ts +13 -0
- package/src/input-classes.ts +9 -0
- package/src/list-classes.test.ts +23 -0
- package/src/list-classes.ts +26 -0
- package/src/radio-card-classes.test.ts +12 -0
- package/src/radio-card-classes.ts +9 -0
- package/src/search-input-classes.test.ts +12 -0
- package/src/search-input-classes.ts +9 -0
- package/src/segmented-tabs-classes.test.ts +12 -0
- package/src/segmented-tabs-classes.ts +9 -0
- package/src/switch-classes.test.ts +9 -0
- package/src/switch-classes.ts +9 -0
- package/src/tabs-classes.test.ts +12 -0
- package/src/tabs-classes.ts +9 -0
package/src/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
|
+
}
|
package/src/Callout.tsx
ADDED
|
@@ -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
|
+
});
|