@sproutsocial/seeds-react-tree 0.4.0 → 0.4.3
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/.turbo/turbo-build.log +9 -10
- package/CHANGELOG.md +33 -0
- package/dist/esm/index.js +106 -281
- package/dist/esm/index.js.map +1 -1
- package/dist/index.js +106 -281
- package/dist/index.js.map +1 -1
- package/dist/tree.css +318 -0
- package/package.json +11 -10
- package/src/Common/TreeSearchableSelectBase.tsx +36 -119
- package/src/Tree.stories.tsx +26 -0
- package/src/Tree.tsx +4 -4
- package/src/TreeCombobox.tsx +17 -71
- package/src/TreeItem.tsx +33 -22
- package/src/__tests__/Tree.test.tsx +61 -0
- package/src/cn.ts +28 -0
- package/src/tree.css +318 -0
- package/src/TreeStyles.tsx +0 -115
package/src/TreeCombobox.tsx
CHANGED
|
@@ -1,71 +1,12 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import styled from "styled-components";
|
|
3
2
|
import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
4
|
-
import { focusRing } from "@sproutsocial/seeds-react-mixins";
|
|
5
3
|
import { Tree, type TreeProps } from "./Tree";
|
|
6
4
|
import { TreeItem } from "./TreeItem";
|
|
5
|
+
import { cn } from "./cn";
|
|
7
6
|
import { filterTree } from "./Common/filterTree";
|
|
8
7
|
import { computeTreeNavigation, treeItemDomId } from "./Common/treeNavigation";
|
|
9
8
|
import type { TreeItemData } from "./Common/types";
|
|
10
9
|
|
|
11
|
-
const Root = styled.div`
|
|
12
|
-
display: flex;
|
|
13
|
-
flex-direction: column;
|
|
14
|
-
flex: 1;
|
|
15
|
-
min-height: 0;
|
|
16
|
-
`;
|
|
17
|
-
|
|
18
|
-
const SearchSection = styled.div`
|
|
19
|
-
padding: ${({ theme }) => theme.space[300]};
|
|
20
|
-
border-bottom: 1px solid ${({ theme }) => theme.colors.container.border.base};
|
|
21
|
-
flex-shrink: 0;
|
|
22
|
-
`;
|
|
23
|
-
|
|
24
|
-
const InputGroup = styled.div`
|
|
25
|
-
display: flex;
|
|
26
|
-
align-items: center;
|
|
27
|
-
gap: ${({ theme }) => theme.space[200]};
|
|
28
|
-
padding: ${({ theme }) => theme.space[200]} ${({ theme }) => theme.space[300]};
|
|
29
|
-
border-radius: ${({ theme }) => theme.radii[500]};
|
|
30
|
-
border: 1px solid ${({ theme }) => theme.colors.container.border.base};
|
|
31
|
-
background: ${({ theme }) => theme.colors.container.background.base};
|
|
32
|
-
color: ${({ theme }) => theme.colors.icon.base};
|
|
33
|
-
|
|
34
|
-
&:focus-within {
|
|
35
|
-
${focusRing}
|
|
36
|
-
}
|
|
37
|
-
`;
|
|
38
|
-
|
|
39
|
-
const ComboboxInput = styled.input`
|
|
40
|
-
flex: 1;
|
|
41
|
-
border: none;
|
|
42
|
-
background: transparent;
|
|
43
|
-
outline: none;
|
|
44
|
-
font-family: ${({ theme }) => theme.fontFamily};
|
|
45
|
-
font-size: ${({ theme }) => theme.typography[200].fontSize};
|
|
46
|
-
line-height: ${({ theme }) => theme.typography[200].lineHeight};
|
|
47
|
-
color: ${({ theme }) => theme.colors.text.body};
|
|
48
|
-
|
|
49
|
-
&::placeholder {
|
|
50
|
-
color: ${({ theme }) => theme.colors.text.subtext};
|
|
51
|
-
}
|
|
52
|
-
`;
|
|
53
|
-
|
|
54
|
-
const ListSection = styled.div`
|
|
55
|
-
padding: ${({ theme }) => theme.space[200]};
|
|
56
|
-
overflow: auto;
|
|
57
|
-
flex: 1;
|
|
58
|
-
min-height: 0;
|
|
59
|
-
`;
|
|
60
|
-
|
|
61
|
-
const EmptyState = styled.div`
|
|
62
|
-
padding: ${({ theme }) => `${theme.space[300]} ${theme.space[350]}`};
|
|
63
|
-
text-align: center;
|
|
64
|
-
color: ${({ theme }) => theme.colors.text.subtext};
|
|
65
|
-
font-size: ${({ theme }) => theme.typography[200].fontSize};
|
|
66
|
-
line-height: ${({ theme }) => theme.typography[200].lineHeight};
|
|
67
|
-
`;
|
|
68
|
-
|
|
69
10
|
const NAV_KEYS = new Set([
|
|
70
11
|
"ArrowDown",
|
|
71
12
|
"ArrowUp",
|
|
@@ -256,11 +197,12 @@ export function TreeCombobox(props: TreeComboboxProps) {
|
|
|
256
197
|
const showEmpty = visibleItems.length === 0;
|
|
257
198
|
|
|
258
199
|
return (
|
|
259
|
-
<
|
|
260
|
-
<
|
|
261
|
-
<
|
|
200
|
+
<div className={cn("seeds-tree-combobox", className)}>
|
|
201
|
+
<div className="seeds-tree-combobox-search">
|
|
202
|
+
<div className="seeds-tree-combobox-input-group">
|
|
262
203
|
<Icon name="magnifying-glass-outline" size="small" aria-hidden />
|
|
263
|
-
<
|
|
204
|
+
<input
|
|
205
|
+
className="seeds-tree-combobox-input"
|
|
264
206
|
type="text"
|
|
265
207
|
role="combobox"
|
|
266
208
|
id={id}
|
|
@@ -278,10 +220,10 @@ export function TreeCombobox(props: TreeComboboxProps) {
|
|
|
278
220
|
onChange={(e) => setQuery(e.target.value)}
|
|
279
221
|
onKeyDown={handleInputKeyDown}
|
|
280
222
|
/>
|
|
281
|
-
</
|
|
282
|
-
</
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
283
225
|
|
|
284
|
-
<
|
|
226
|
+
<div className="seeds-tree-combobox-list">
|
|
285
227
|
<Tree
|
|
286
228
|
ref={treeRef}
|
|
287
229
|
aria-label={ariaLabel}
|
|
@@ -305,12 +247,16 @@ export function TreeCombobox(props: TreeComboboxProps) {
|
|
|
305
247
|
</Tree>
|
|
306
248
|
|
|
307
249
|
{showEmpty ? (
|
|
308
|
-
<
|
|
250
|
+
<div
|
|
251
|
+
className="seeds-tree-combobox-empty"
|
|
252
|
+
role="status"
|
|
253
|
+
aria-live="polite"
|
|
254
|
+
>
|
|
309
255
|
{emptyText}
|
|
310
|
-
</
|
|
256
|
+
</div>
|
|
311
257
|
) : null}
|
|
312
|
-
</
|
|
313
|
-
</
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
314
260
|
);
|
|
315
261
|
}
|
|
316
262
|
|
package/src/TreeItem.tsx
CHANGED
|
@@ -3,14 +3,7 @@ import { Icon } from "@sproutsocial/seeds-react-icon";
|
|
|
3
3
|
import { TreeItemContext, useTreeContext } from "./Common/treeContext";
|
|
4
4
|
import { useTreeKeyboard } from "./Common/useTreeKeyboard";
|
|
5
5
|
import { treeItemDomId } from "./Common/treeNavigation";
|
|
6
|
-
import {
|
|
7
|
-
TreeItemChevron,
|
|
8
|
-
TreeItemEl,
|
|
9
|
-
TreeItemGroup,
|
|
10
|
-
TreeItemIcon,
|
|
11
|
-
TreeItemLabel,
|
|
12
|
-
TreeItemRow,
|
|
13
|
-
} from "./TreeStyles";
|
|
6
|
+
import { cn } from "./cn";
|
|
14
7
|
|
|
15
8
|
export type TreeItemProps = {
|
|
16
9
|
id: string;
|
|
@@ -127,8 +120,9 @@ export function TreeItem(props: TreeItemProps) {
|
|
|
127
120
|
const labelId = `${id}__label`;
|
|
128
121
|
|
|
129
122
|
return (
|
|
130
|
-
<
|
|
123
|
+
<li
|
|
131
124
|
ref={itemRef}
|
|
125
|
+
className="seeds-tree-item"
|
|
132
126
|
id={treeItemDomId(id)}
|
|
133
127
|
role="treeitem"
|
|
134
128
|
data-treeitem-id={id}
|
|
@@ -174,11 +168,13 @@ export function TreeItem(props: TreeItemProps) {
|
|
|
174
168
|
if (ctx.focusedId !== id) ctx.setFocusedId(id);
|
|
175
169
|
}}
|
|
176
170
|
>
|
|
177
|
-
<
|
|
171
|
+
<div
|
|
178
172
|
data-treeitem-row
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
173
|
+
className={cn("seeds-tree-item-row", {
|
|
174
|
+
selected: isSelected,
|
|
175
|
+
disabled,
|
|
176
|
+
})}
|
|
177
|
+
style={{ "--tree-level": level } as React.CSSProperties}
|
|
182
178
|
>
|
|
183
179
|
{ctx.renderSelectionIndicator &&
|
|
184
180
|
!(ctx.selectableNodes === "leaves" && hasChildren)
|
|
@@ -189,29 +185,44 @@ export function TreeItem(props: TreeItemProps) {
|
|
|
189
185
|
})
|
|
190
186
|
: null}
|
|
191
187
|
|
|
192
|
-
{icon != null ?
|
|
193
|
-
|
|
188
|
+
{icon != null ? (
|
|
189
|
+
<span className="seeds-tree-item-icon" aria-hidden>
|
|
190
|
+
{icon}
|
|
191
|
+
</span>
|
|
192
|
+
) : null}
|
|
193
|
+
<span className="seeds-tree-item-label" id={labelId}>
|
|
194
|
+
{label}
|
|
195
|
+
</span>
|
|
194
196
|
|
|
195
197
|
{hasChildren ? (
|
|
196
|
-
<
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
tabIndex={-1}
|
|
197
201
|
data-tree-chevron
|
|
198
202
|
aria-hidden
|
|
199
|
-
|
|
203
|
+
className={cn("seeds-tree-item-chevron", {
|
|
204
|
+
expanded: isExpanded,
|
|
205
|
+
})}
|
|
200
206
|
onClick={handleChevronClick}
|
|
201
207
|
>
|
|
202
208
|
<Icon name="chevron-down-outline" size="small" />
|
|
203
|
-
</
|
|
209
|
+
</button>
|
|
204
210
|
) : null}
|
|
205
|
-
</
|
|
211
|
+
</div>
|
|
206
212
|
|
|
207
213
|
{hasChildren ? (
|
|
208
|
-
<
|
|
214
|
+
<ul
|
|
215
|
+
className="seeds-tree-item-group"
|
|
216
|
+
role="group"
|
|
217
|
+
id={groupId}
|
|
218
|
+
hidden={!isExpanded}
|
|
219
|
+
>
|
|
209
220
|
<TreeItemContext.Provider value={{ level: level + 1 }}>
|
|
210
221
|
{children}
|
|
211
222
|
</TreeItemContext.Provider>
|
|
212
|
-
</
|
|
223
|
+
</ul>
|
|
213
224
|
) : null}
|
|
214
|
-
</
|
|
225
|
+
</li>
|
|
215
226
|
);
|
|
216
227
|
}
|
|
217
228
|
|
|
@@ -197,3 +197,64 @@ describe("Tree", () => {
|
|
|
197
197
|
await runA11yCheck();
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
|
+
|
|
201
|
+
describe("Tree CSS-class rendering (Tailwind migration)", () => {
|
|
202
|
+
it("renders Seeds CSS classes on the root and item elements", () => {
|
|
203
|
+
renderBasicTree();
|
|
204
|
+
|
|
205
|
+
const root = screen.getByRole("tree", { name: "Files" });
|
|
206
|
+
expect(root).toHaveClass("seeds-tree");
|
|
207
|
+
|
|
208
|
+
const srcItem = screen.getByRole("treeitem", { name: /src/ });
|
|
209
|
+
expect(srcItem).toHaveClass("seeds-tree-item");
|
|
210
|
+
|
|
211
|
+
const row = srcItem.querySelector(".seeds-tree-item-row");
|
|
212
|
+
expect(row).not.toBeNull();
|
|
213
|
+
expect(srcItem.querySelector(".seeds-tree-item-label")).not.toBeNull();
|
|
214
|
+
|
|
215
|
+
// Expanded branches expose a chevron button with the `expanded` modifier.
|
|
216
|
+
const chevron = row!.querySelector(".seeds-tree-item-chevron");
|
|
217
|
+
expect(chevron).toHaveClass("expanded");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("merges a consumer className onto the root without dropping seeds-tree", () => {
|
|
221
|
+
renderBasicTree({ className: "my-tree" });
|
|
222
|
+
const root = screen.getByRole("tree", { name: "Files" });
|
|
223
|
+
expect(root).toHaveClass("seeds-tree");
|
|
224
|
+
expect(root).toHaveClass("my-tree");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("sets the --tree-level custom property per depth for indentation", () => {
|
|
228
|
+
renderBasicTree();
|
|
229
|
+
const rootRow = screen
|
|
230
|
+
.getByRole("treeitem", { name: /src/ })
|
|
231
|
+
.querySelector<HTMLElement>(".seeds-tree-item-row");
|
|
232
|
+
const childRow = screen
|
|
233
|
+
.getByRole("treeitem", { name: /Button\.tsx/ })
|
|
234
|
+
.querySelector<HTMLElement>(".seeds-tree-item-row");
|
|
235
|
+
expect(rootRow!.style.getPropertyValue("--tree-level")).toBe("1");
|
|
236
|
+
expect(childRow!.style.getPropertyValue("--tree-level")).toBe("2");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("applies the `selected` modifier class on a selected row", async () => {
|
|
240
|
+
const { user } = render(
|
|
241
|
+
<Tree aria-label="Files" selectionMode="single">
|
|
242
|
+
<TreeItem id="a" label="A" />
|
|
243
|
+
</Tree>
|
|
244
|
+
);
|
|
245
|
+
const a = screen.getByRole("treeitem", { name: "A" });
|
|
246
|
+
a.focus();
|
|
247
|
+
await user.keyboard("{Enter}");
|
|
248
|
+
expect(a.querySelector(".seeds-tree-item-row")).toHaveClass("selected");
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("applies the `disabled` modifier class on a disabled row", () => {
|
|
252
|
+
render(
|
|
253
|
+
<Tree aria-label="Files">
|
|
254
|
+
<TreeItem id="a" label="A" disabled />
|
|
255
|
+
</Tree>
|
|
256
|
+
);
|
|
257
|
+
const a = screen.getByRole("treeitem", { name: "A" });
|
|
258
|
+
expect(a.querySelector(".seeds-tree-item-row")).toHaveClass("disabled");
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/cn.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local className merge helper.
|
|
3
|
+
*
|
|
4
|
+
* `cn` is intentionally NOT imported from `@sproutsocial/seeds-react-utilities`
|
|
5
|
+
* (it isn't exported there); each Tailwind/CSS-class Seeds component keeps its
|
|
6
|
+
* own copy, matching the `seeds-react-button` reference.
|
|
7
|
+
*/
|
|
8
|
+
export function cn(
|
|
9
|
+
...inputs: (string | undefined | null | false | Record<string, boolean>)[]
|
|
10
|
+
): string {
|
|
11
|
+
const classes: string[] = [];
|
|
12
|
+
|
|
13
|
+
for (const input of inputs) {
|
|
14
|
+
if (!input) continue;
|
|
15
|
+
|
|
16
|
+
if (typeof input === "string") {
|
|
17
|
+
classes.push(input);
|
|
18
|
+
} else if (typeof input === "object") {
|
|
19
|
+
for (const [key, value] of Object.entries(input)) {
|
|
20
|
+
if (value) {
|
|
21
|
+
classes.push(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return classes.join(" ");
|
|
28
|
+
}
|
package/src/tree.css
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds Tree component classes
|
|
3
|
+
* CSS-class mirror of the styled-components that previously styled the tree
|
|
4
|
+
* package (TreeStyles.tsx, TreeCombobox.tsx, Common/TreeSearchableSelectBase.tsx).
|
|
5
|
+
*
|
|
6
|
+
* Requires @sproutsocial/seeds-react-theme/dist/theme-all.css imported for
|
|
7
|
+
* CSS variable definitions and dark mode support.
|
|
8
|
+
*
|
|
9
|
+
* Rules live in @layer components so consumer Tailwind utilities (which sit in
|
|
10
|
+
* the later `utilities` layer) can override component styles on equal
|
|
11
|
+
* specificity. Without the layer, these unlayered rules would out-rank any
|
|
12
|
+
* utility class.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* <ul class="seeds-tree" role="tree">
|
|
16
|
+
* <li class="seeds-tree-item" role="treeitem">
|
|
17
|
+
* <div class="seeds-tree-item-row" style="--tree-level: 1">…</div>
|
|
18
|
+
* </li>
|
|
19
|
+
* </ul>
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
@layer components {
|
|
23
|
+
/* ---------------------------------------------------------------------- */
|
|
24
|
+
/* Tree / TreeItem (mirror of TreeStyles.tsx) */
|
|
25
|
+
/* ---------------------------------------------------------------------- */
|
|
26
|
+
|
|
27
|
+
.seeds-tree {
|
|
28
|
+
list-style: none;
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
font-family: var(--font-family);
|
|
32
|
+
font-size: var(--font-size-200);
|
|
33
|
+
line-height: var(--line-height-200);
|
|
34
|
+
color: var(--color-text-body);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.seeds-tree-item {
|
|
38
|
+
list-style: none;
|
|
39
|
+
outline: none;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/*
|
|
43
|
+
* Tree rows stack tightly, so the standard outset Seeds focusRing bleeds
|
|
44
|
+
* into adjacent rows. Use an inset outline so the ring is drawn just inside
|
|
45
|
+
* the row's edge and never overlaps siblings or children.
|
|
46
|
+
*
|
|
47
|
+
* The same ring is drawn when [data-treeitem-active] is set so combobox
|
|
48
|
+
* hosts that drive the tree via aria-activedescendant (no real DOM focus)
|
|
49
|
+
* still get a visible "active" indicator on the row.
|
|
50
|
+
*/
|
|
51
|
+
.seeds-tree-item:focus-visible > .seeds-tree-item-row,
|
|
52
|
+
.seeds-tree-item[data-treeitem-active] > .seeds-tree-item-row {
|
|
53
|
+
outline: 2px solid var(--color-button-primary-bg-base);
|
|
54
|
+
outline-offset: -2px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.seeds-tree-item-row {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: var(--space-300);
|
|
61
|
+
padding: var(--space-300);
|
|
62
|
+
/* Indentation per depth: --tree-level is set inline on each row. */
|
|
63
|
+
padding-left: calc(
|
|
64
|
+
var(--space-300) + (var(--tree-level) - 1) * var(--space-400)
|
|
65
|
+
);
|
|
66
|
+
border-radius: var(--radius-400);
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
user-select: none;
|
|
69
|
+
background: transparent;
|
|
70
|
+
transition: background-color var(--duration-fast) var(--ease-ease_in);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.seeds-tree-item-row:hover {
|
|
74
|
+
background: var(--color-listItem-bg-hover);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.seeds-tree-item-row.selected {
|
|
78
|
+
background: var(--color-listItem-bg-hover);
|
|
79
|
+
font-weight: var(--font-weight-semibold);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.seeds-tree-item-row.disabled {
|
|
83
|
+
opacity: 0.4;
|
|
84
|
+
cursor: not-allowed;
|
|
85
|
+
pointer-events: none;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.seeds-tree-item-group {
|
|
89
|
+
list-style: none;
|
|
90
|
+
margin: 0;
|
|
91
|
+
padding: 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.seeds-tree-item-label {
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
overflow: hidden;
|
|
98
|
+
text-overflow: ellipsis;
|
|
99
|
+
white-space: nowrap;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.seeds-tree-item-icon {
|
|
103
|
+
display: inline-flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.seeds-tree-item-chevron {
|
|
109
|
+
display: inline-flex;
|
|
110
|
+
align-items: center;
|
|
111
|
+
justify-content: center;
|
|
112
|
+
flex-shrink: 0;
|
|
113
|
+
width: var(--space-500);
|
|
114
|
+
height: var(--space-500);
|
|
115
|
+
padding: 0;
|
|
116
|
+
border: none;
|
|
117
|
+
background: transparent;
|
|
118
|
+
color: var(--color-icon-base);
|
|
119
|
+
cursor: pointer;
|
|
120
|
+
border-radius: var(--radius-300);
|
|
121
|
+
transition: transform var(--duration-fast) var(--ease-ease_in);
|
|
122
|
+
transform: rotate(-90deg);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.seeds-tree-item-chevron.expanded {
|
|
126
|
+
transform: rotate(0deg);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.seeds-tree-item-chevron:hover {
|
|
130
|
+
background: var(--color-listItem-bg-hover);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* ---------------------------------------------------------------------- */
|
|
134
|
+
/* TreeCombobox (mirror of TreeCombobox.tsx) */
|
|
135
|
+
/* ---------------------------------------------------------------------- */
|
|
136
|
+
|
|
137
|
+
.seeds-tree-combobox {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
flex: 1;
|
|
141
|
+
min-height: 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.seeds-tree-combobox-search {
|
|
145
|
+
padding: var(--space-300);
|
|
146
|
+
border-bottom: 1px solid var(--color-container-border-base);
|
|
147
|
+
flex-shrink: 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.seeds-tree-combobox-input-group {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: var(--space-200);
|
|
154
|
+
padding: var(--space-200) var(--space-300);
|
|
155
|
+
border-radius: var(--radius-500);
|
|
156
|
+
border: 1px solid var(--color-container-border-base);
|
|
157
|
+
background: var(--color-container-bg-base);
|
|
158
|
+
color: var(--color-icon-base);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.seeds-tree-combobox-input-group:focus-within {
|
|
162
|
+
box-shadow:
|
|
163
|
+
0 0 0 1px var(--color-button-primary-bg-base),
|
|
164
|
+
0 0px 0px 4px
|
|
165
|
+
color-mix(
|
|
166
|
+
in srgb,
|
|
167
|
+
var(--color-button-primary-bg-base),
|
|
168
|
+
transparent 70%
|
|
169
|
+
);
|
|
170
|
+
outline: none;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.seeds-tree-combobox-input {
|
|
174
|
+
flex: 1;
|
|
175
|
+
border: none;
|
|
176
|
+
background: transparent;
|
|
177
|
+
outline: none;
|
|
178
|
+
font-family: var(--font-family);
|
|
179
|
+
font-size: var(--font-size-200);
|
|
180
|
+
line-height: var(--line-height-200);
|
|
181
|
+
color: var(--color-text-body);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.seeds-tree-combobox-input::placeholder {
|
|
185
|
+
color: var(--color-text-subtext);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.seeds-tree-combobox-list {
|
|
189
|
+
padding: var(--space-200);
|
|
190
|
+
overflow: auto;
|
|
191
|
+
flex: 1;
|
|
192
|
+
min-height: 0;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.seeds-tree-combobox-empty {
|
|
196
|
+
padding: var(--space-300) var(--space-350);
|
|
197
|
+
text-align: center;
|
|
198
|
+
color: var(--color-text-subtext);
|
|
199
|
+
font-size: var(--font-size-200);
|
|
200
|
+
line-height: var(--line-height-200);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* ---------------------------------------------------------------------- */
|
|
204
|
+
/* TreeSearchableSelect (mirror of Common/TreeSearchableSelectBase.tsx) */
|
|
205
|
+
/* ---------------------------------------------------------------------- */
|
|
206
|
+
|
|
207
|
+
.seeds-tree-select-trigger {
|
|
208
|
+
display: flex;
|
|
209
|
+
align-items: center;
|
|
210
|
+
justify-content: space-between;
|
|
211
|
+
gap: var(--space-300);
|
|
212
|
+
padding: var(--space-200) var(--space-350);
|
|
213
|
+
min-width: 160px;
|
|
214
|
+
width: auto;
|
|
215
|
+
border-radius: var(--radius-500);
|
|
216
|
+
border: 1px solid var(--color-form-border-base);
|
|
217
|
+
background: var(--color-form-bg-base);
|
|
218
|
+
font-family: var(--font-family);
|
|
219
|
+
font-size: var(--font-size-200);
|
|
220
|
+
line-height: var(--line-height-200);
|
|
221
|
+
color: var(--color-text-body);
|
|
222
|
+
text-align: left;
|
|
223
|
+
cursor: default;
|
|
224
|
+
outline: none;
|
|
225
|
+
user-select: none;
|
|
226
|
+
transition:
|
|
227
|
+
border-color var(--duration-fast) var(--ease-ease_in),
|
|
228
|
+
box-shadow var(--duration-fast) var(--ease-ease_in);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.seeds-tree-select-trigger.full-width {
|
|
232
|
+
width: 100%;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.seeds-tree-select-trigger:focus-visible {
|
|
236
|
+
box-shadow:
|
|
237
|
+
0 0 0 1px var(--color-button-primary-bg-base),
|
|
238
|
+
0 0px 0px 4px
|
|
239
|
+
color-mix(
|
|
240
|
+
in srgb,
|
|
241
|
+
var(--color-button-primary-bg-base),
|
|
242
|
+
transparent 70%
|
|
243
|
+
);
|
|
244
|
+
outline: none;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.seeds-tree-select-trigger::-moz-focus-inner {
|
|
248
|
+
border: 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.seeds-tree-select-trigger[aria-disabled="true"],
|
|
252
|
+
.seeds-tree-select-trigger:disabled {
|
|
253
|
+
opacity: 0.4;
|
|
254
|
+
cursor: not-allowed;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.seeds-tree-select-trigger.invalid {
|
|
258
|
+
border-color: var(--color-form-border-error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.seeds-tree-select-trigger-label {
|
|
262
|
+
flex: 1;
|
|
263
|
+
min-width: 0;
|
|
264
|
+
overflow: hidden;
|
|
265
|
+
text-overflow: ellipsis;
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.seeds-tree-select-placeholder {
|
|
270
|
+
color: var(--color-text-subtext);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.seeds-tree-select-chevron {
|
|
274
|
+
display: inline-flex;
|
|
275
|
+
align-items: center;
|
|
276
|
+
color: var(--color-icon-base);
|
|
277
|
+
transition: transform var(--duration-fast) var(--ease-ease_in);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.seeds-tree-select-chevron.open {
|
|
281
|
+
transform: rotate(-180deg);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/*
|
|
285
|
+
* Visually-hidden input that mirrors the combobox's selected value into a
|
|
286
|
+
* form-submittable element. Matches Base UI's pattern (used by v3
|
|
287
|
+
* SearchableSelect): the input stays in the DOM so HTML forms pick up the
|
|
288
|
+
* value, but it's removed from view and from the accessibility tree.
|
|
289
|
+
*/
|
|
290
|
+
.seeds-tree-select-hidden-input {
|
|
291
|
+
clip-path: inset(50%);
|
|
292
|
+
overflow: hidden;
|
|
293
|
+
white-space: nowrap;
|
|
294
|
+
border: 0;
|
|
295
|
+
padding: 0;
|
|
296
|
+
width: 1px;
|
|
297
|
+
height: 1px;
|
|
298
|
+
margin: -1px;
|
|
299
|
+
position: fixed;
|
|
300
|
+
top: 0;
|
|
301
|
+
left: 0;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.seeds-tree-select-popout {
|
|
305
|
+
display: flex;
|
|
306
|
+
flex-direction: column;
|
|
307
|
+
/* Width sizes to content within bounds: at least the trigger width, never */
|
|
308
|
+
/* wider than the available viewport space. Shallow popups stay flush with */
|
|
309
|
+
/* the trigger; deeply nested trees expand horizontally as needed. */
|
|
310
|
+
min-width: var(--radix-popover-trigger-width);
|
|
311
|
+
max-width: var(--radix-popper-available-width);
|
|
312
|
+
/* Height caps at 400px (or available viewport) so a popup with many */
|
|
313
|
+
/* expanded branches scrolls vertically instead of growing off-screen. */
|
|
314
|
+
max-height: min(var(--radix-popper-available-height), 400px);
|
|
315
|
+
/* Width is set inline via --tree-select-width (defaults to max-content). */
|
|
316
|
+
width: var(--tree-select-width, max-content);
|
|
317
|
+
}
|
|
318
|
+
}
|