fluentui-svelte 0.3.0 → 0.5.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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/components/auto-suggest-box/docs/llms.md +76 -15
  3. package/dist/components/breadcrumb/breadcrumb-button.svelte +130 -0
  4. package/dist/components/breadcrumb/breadcrumb-button.svelte.d.ts +35 -0
  5. package/dist/components/breadcrumb/breadcrumb-divider.svelte +39 -0
  6. package/dist/components/breadcrumb/breadcrumb-divider.svelte.d.ts +14 -0
  7. package/dist/components/breadcrumb/breadcrumb-item.svelte +30 -0
  8. package/dist/components/breadcrumb/breadcrumb-item.svelte.d.ts +16 -0
  9. package/dist/components/breadcrumb/breadcrumb.svelte +101 -0
  10. package/dist/components/breadcrumb/breadcrumb.svelte.d.ts +25 -0
  11. package/dist/components/breadcrumb/docs/llms.md +193 -0
  12. package/dist/components/breadcrumb/types.d.ts +80 -0
  13. package/dist/components/breadcrumb/types.js +1 -0
  14. package/dist/components/card/card-header.svelte +2 -2
  15. package/dist/components/dropdown/dropdown.svelte +1 -0
  16. package/dist/components/list-view/docs/llms.md +79 -27
  17. package/dist/components/list-view/list-view-item.svelte +14 -11
  18. package/dist/components/list-view/list-view.svelte +3 -2
  19. package/dist/components/list-view/types.d.ts +5 -1
  20. package/dist/components/menu/docs/llms.md +16 -0
  21. package/dist/components/menu/menu-popover.svelte +7 -12
  22. package/dist/components/menu/menu-popover.svelte.d.ts +2 -10
  23. package/dist/components/menu/types.d.ts +21 -0
  24. package/dist/components/tab-view/docs/llms.md +146 -0
  25. package/dist/components/tab-view/tab-view-tab.svelte +290 -0
  26. package/dist/components/tab-view/tab-view-tab.svelte.d.ts +20 -0
  27. package/dist/components/tab-view/tab-view.svelte +188 -0
  28. package/dist/components/tab-view/tab-view.svelte.d.ts +26 -0
  29. package/dist/components/tab-view/tab-view.svelte.js +15 -0
  30. package/dist/components/tab-view/types.d.ts +79 -0
  31. package/dist/components/tab-view/types.js +1 -0
  32. package/dist/components/top-nav/docs/llms.md +176 -0
  33. package/dist/components/top-nav/top-nav-item.svelte +234 -0
  34. package/dist/components/top-nav/top-nav-item.svelte.d.ts +41 -0
  35. package/dist/components/top-nav/top-nav.svelte +259 -0
  36. package/dist/components/top-nav/top-nav.svelte.d.ts +26 -0
  37. package/dist/components/top-nav/top-nav.svelte.js +18 -0
  38. package/dist/components/top-nav/types.d.ts +89 -0
  39. package/dist/components/top-nav/types.js +1 -0
  40. package/dist/components/tree-view/docs/llms.md +166 -1
  41. package/dist/components/tree-view/tree-view-item-content.svelte +41 -8
  42. package/dist/components/tree-view/tree-view-item.svelte +10 -1
  43. package/dist/components/tree-view/tree-view.svelte +17 -2
  44. package/dist/components/tree-view/tree-view.svelte.js +4 -2
  45. package/dist/components/tree-view/types.d.ts +4 -0
  46. package/dist/css/class-darkmode.css +1 -1
  47. package/dist/css/media-darkmode.css +1 -1
  48. package/dist/css/theme.css +1 -1
  49. package/dist/index.d.ts +8 -0
  50. package/dist/index.js +14 -0
  51. package/package.json +2 -1
@@ -0,0 +1,259 @@
1
+ <script lang="ts">
2
+ import { getTabspotAttributes } from 'tabspot';
3
+ import { setTopNavContext, TABSPOT_ITEMS, TABSPOT_SKIP } from './top-nav.svelte.js';
4
+ import type { TopNavContext, TopNavProps } from './types.js';
5
+
6
+ let {
7
+ ref = $bindable(),
8
+ selectedValue = $bindable(''),
9
+ onTabSelect,
10
+ appearance = 'transparent',
11
+ size = 'medium',
12
+ vertical = false,
13
+ disabled = false,
14
+ selectTabOnFocus = false,
15
+ reserveSelectedTabSpace = true,
16
+ disableTabspot = false,
17
+ class: classes,
18
+ children,
19
+ ...attributes
20
+ }: TopNavProps = $props();
21
+
22
+ const context: TopNavContext = $state({
23
+ config: {
24
+ get appearance() {
25
+ return appearance;
26
+ },
27
+ get size() {
28
+ return size;
29
+ },
30
+ get vertical() {
31
+ return vertical;
32
+ },
33
+ get disabled() {
34
+ return disabled;
35
+ },
36
+ get selectTabOnFocus() {
37
+ return selectTabOnFocus;
38
+ },
39
+ get reserveSelectedTabSpace() {
40
+ return reserveSelectedTabSpace;
41
+ }
42
+ },
43
+ state: {
44
+ get selectedValue() {
45
+ return selectedValue;
46
+ },
47
+ set selectedValue(value: string) {
48
+ selectedValue = value;
49
+ }
50
+ },
51
+ events: null,
52
+ methods: {
53
+ selectTab: (event: Event, value: string) => {
54
+ if (selectedValue === value) return;
55
+ selectedValue = value;
56
+ onTabSelect?.(event, value);
57
+ }
58
+ }
59
+ });
60
+
61
+ setTopNavContext(context);
62
+
63
+ /**
64
+ * A single tab stop for the whole strip: the arrows walk the items along the axis they are laid
65
+ * out on, and `Home`/`End` jump to the ends.
66
+ */
67
+ const tabspotAttributes = $derived(
68
+ !disableTabspot &&
69
+ getTabspotAttributes({
70
+ root: { manageSpecialKeys: { Home: true, End: true } },
71
+ mover: {
72
+ axis: vertical ? 'vertical' : 'horizontal',
73
+ items: TABSPOT_ITEMS,
74
+ skip: TABSPOT_SKIP
75
+ }
76
+ })
77
+ );
78
+
79
+ /** The circular appearances fill the selected item instead of underlining it. */
80
+ const hasIndicator = $derived(appearance === 'transparent' || appearance === 'subtle');
81
+
82
+ /** Where the bar sits along the axis, and how far it reaches. Both are read off the DOM. */
83
+ let offset = $state(0);
84
+ let extent = $state(0);
85
+ let measured = $state(false);
86
+
87
+ /**
88
+ * The bar belongs to the strip rather than to the selected item, which is what lets it travel:
89
+ * one element that stays mounted and moves, instead of one per item appearing and disappearing.
90
+ * That costs a measurement, since only the DOM knows where an item ended up.
91
+ *
92
+ * It spans the content of the item rather than its whole box, so the padding is taken off both
93
+ * ends — the same padding the item lays its own hover bar within.
94
+ */
95
+ const measure = () => {
96
+ const root = ref;
97
+ if (!root) return;
98
+
99
+ const item = [...root.querySelectorAll<HTMLElement>(TABSPOT_ITEMS)].find(
100
+ (el) => el.dataset.value === selectedValue
101
+ );
102
+ if (!item) {
103
+ measured = false;
104
+ return;
105
+ }
106
+
107
+ const style = getComputedStyle(item);
108
+ const before = parseFloat(vertical ? style.paddingTop : style.paddingLeft) || 0;
109
+ const after = parseFloat(vertical ? style.paddingBottom : style.paddingRight) || 0;
110
+
111
+ offset = (vertical ? item.offsetTop : item.offsetLeft) + before;
112
+ extent = Math.max(0, (vertical ? item.offsetHeight : item.offsetWidth) - before - after);
113
+ measured = true;
114
+ };
115
+
116
+ // The selection and the axis are what move the bar; both are read through `measure`.
117
+ $effect(() => {
118
+ measure();
119
+ });
120
+
121
+ /**
122
+ * Everything else that moves it does so through the layout: a label that rewrapped, a font that
123
+ * finished loading, an item that was added or removed. The observers are what catch those, since
124
+ * none of them is a change to a prop.
125
+ */
126
+ $effect(() => {
127
+ const root = ref;
128
+ if (!root || !hasIndicator) return;
129
+
130
+ const resize = new ResizeObserver(measure);
131
+ const watch = () => {
132
+ resize.disconnect();
133
+ resize.observe(root);
134
+ for (const item of root.querySelectorAll(TABSPOT_ITEMS)) resize.observe(item);
135
+ };
136
+ const mutations = new MutationObserver(() => {
137
+ watch();
138
+ measure();
139
+ });
140
+
141
+ watch();
142
+ mutations.observe(root, { childList: true, subtree: true });
143
+
144
+ return () => {
145
+ resize.disconnect();
146
+ mutations.disconnect();
147
+ };
148
+ });
149
+ </script>
150
+
151
+ <!--
152
+ @component
153
+ A top nav gives a single selection from a row of items, with a bar that travels to whichever one
154
+ is selected. It owns the selection alone: what an item opens is rendered by the consumer.
155
+
156
+ - Usage:
157
+ ```tsx
158
+ <script>
159
+ import { TopNav, TopNavItem } from 'fluentui-svelte';
160
+
161
+ let selectedValue = $state('arrivals');
162
+ </script>
163
+
164
+ <TopNav bind:selectedValue aria-label="Flights">
165
+ <TopNavItem value="arrivals">Arrivals</TopNavItem>
166
+ <TopNavItem value="departures">Departures</TopNavItem>
167
+ </TopNav>
168
+
169
+ {#if selectedValue === 'arrivals'}
170
+ <p>Arrivals</p>
171
+ {/if}
172
+ ```
173
+ -->
174
+ <div
175
+ bind:this={ref}
176
+ role="tablist"
177
+ aria-orientation={vertical ? 'vertical' : undefined}
178
+ class={['fs-top-nav', appearance, size, { vertical }, classes]}
179
+ {...tabspotAttributes}
180
+ {...attributes}
181
+ >
182
+ {@render children?.()}
183
+ {#if hasIndicator && measured}
184
+ <span
185
+ class="fs-top-nav-indicator"
186
+ aria-hidden="true"
187
+ style:--fs-top-nav-indicator-offset="{offset}px"
188
+ style:--fs-top-nav-indicator-extent="{extent}px"
189
+ ></span>
190
+ {/if}
191
+ </div>
192
+
193
+ <style>
194
+ .fs-top-nav {
195
+ position: relative;
196
+ display: flex;
197
+ align-items: stretch;
198
+ font-family: var(--fs-font-family-base);
199
+ color: var(--fs-text-primary);
200
+ /* Read by every item, so a size change ripples out of the strip alone. */
201
+ --fs-top-nav-item-padding-inline: 0.625rem;
202
+ --fs-top-nav-item-padding-block: 0.375rem;
203
+ --fs-top-nav-item-font-size: var(--fs-body2-font-size);
204
+ --fs-top-nav-item-line-height: var(--fs-body2-line-height);
205
+ --fs-top-nav-item-gap: 0.5rem;
206
+ --fs-top-nav-item-icon-size: 1.25rem;
207
+ --fs-top-nav-bar-thickness: 0.1875rem;
208
+ &.small {
209
+ --fs-top-nav-item-padding-inline: 0.5rem;
210
+ --fs-top-nav-item-padding-block: 0.375rem;
211
+ --fs-top-nav-item-font-size: var(--fs-caption-font-size);
212
+ --fs-top-nav-item-line-height: var(--fs-caption-line-height);
213
+ --fs-top-nav-item-gap: 0.375rem;
214
+ --fs-top-nav-item-icon-size: 1rem;
215
+ --fs-top-nav-bar-thickness: 0.125rem;
216
+ }
217
+ &.large {
218
+ --fs-top-nav-item-padding-block: 0.875rem;
219
+ --fs-top-nav-item-font-size: var(--fs-body-font-size);
220
+ --fs-top-nav-item-line-height: var(--fs-body-line-height);
221
+ }
222
+ &.vertical {
223
+ flex-direction: column;
224
+ align-items: stretch;
225
+ }
226
+ }
227
+
228
+ /*
229
+ * The bar. It is placed from the measurement rather than laid out, so moving it is a transform
230
+ * the compositor can carry — and a transform is what keeps it on screen the whole way across,
231
+ * instead of leaving one item and arriving at the next.
232
+ */
233
+ .fs-top-nav-indicator {
234
+ position: absolute;
235
+ left: 0;
236
+ bottom: 0;
237
+ width: var(--fs-top-nav-indicator-extent, 0);
238
+ height: var(--fs-top-nav-bar-thickness);
239
+ border-radius: var(--fs-top-nav-bar-thickness);
240
+ background: var(--fs-accent-fill-default);
241
+ /* `offsetLeft` is measured from the left whichever way the strip is written. */
242
+ transform: translateX(var(--fs-top-nav-indicator-offset, 0));
243
+ transition:
244
+ transform var(--fs-normal-duration) var(--fs-point-to-point),
245
+ width var(--fs-normal-duration) var(--fs-point-to-point),
246
+ height var(--fs-normal-duration) var(--fs-point-to-point);
247
+ pointer-events: none;
248
+ }
249
+ .fs-top-nav.vertical .fs-top-nav-indicator {
250
+ top: 0;
251
+ bottom: auto;
252
+ /* The leading edge, so a strip written right to left grows its bar on the right. */
253
+ left: auto;
254
+ inset-inline-start: 0;
255
+ width: var(--fs-top-nav-bar-thickness);
256
+ height: var(--fs-top-nav-indicator-extent, 0);
257
+ transform: translateY(var(--fs-top-nav-indicator-offset, 0));
258
+ }
259
+ </style>
@@ -0,0 +1,26 @@
1
+ import type { TopNavProps } from './types.ts';
2
+ /**
3
+ * A top nav gives a single selection from a row of items, with a bar that travels to whichever one
4
+ * is selected. It owns the selection alone: what an item opens is rendered by the consumer.
5
+ *
6
+ * - Usage:
7
+ * ```tsx
8
+ * <script>
9
+ * import { TopNav, TopNavItem } from 'fluentui-svelte';
10
+ *
11
+ * let selectedValue = $state('arrivals');
12
+ * </script>
13
+ *
14
+ * <TopNav bind:selectedValue aria-label="Flights">
15
+ * <TopNavItem value="arrivals">Arrivals</TopNavItem>
16
+ * <TopNavItem value="departures">Departures</TopNavItem>
17
+ * </TopNav>
18
+ *
19
+ * {#if selectedValue === 'arrivals'}
20
+ * <p>Arrivals</p>
21
+ * {/if}
22
+ * ```
23
+ */
24
+ declare const TopNav: import("svelte").Component<TopNavProps, {}, "ref" | "selectedValue">;
25
+ type TopNav = ReturnType<typeof TopNav>;
26
+ export default TopNav;
@@ -0,0 +1,18 @@
1
+ import { PREFIX } from '../../internal/constants.js';
2
+ import { createFSContext } from '../../internal/index.js';
3
+ export const COMPONENT_NAME = `${PREFIX}-top-nav`;
4
+ export const [getTopNavContext, setTopNavContext] = createFSContext();
5
+ /** The items a mover lands on, and what the bar is measured against. */
6
+ export const TABSPOT_ITEMS = '.fs-top-nav-item';
7
+ /**
8
+ * Items the cursor passes over instead of landing on: a disabled item answers nothing once reached.
9
+ * A button says so with the attribute of its own; an anchor, which has none, says it to ARIA.
10
+ */
11
+ export const TABSPOT_SKIP = '[disabled], [aria-disabled="true"]';
12
+ /** Reads the top nav context, or fails loudly when an item is rendered outside a `TopNav`. */
13
+ export const requireTopNavContext = () => {
14
+ const context = getTopNavContext();
15
+ if (!context)
16
+ throw new Error(`No TopNavContext found for ${COMPONENT_NAME}.`);
17
+ return context;
18
+ };
@@ -0,0 +1,89 @@
1
+ import type { FSContext } from '../../internal/index.js';
2
+ import type { PolymorphicProps, Sizes } from '../../types/index.js';
3
+ import type { Snippet, Component } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ /** How the strip and every item in it is painted. */
6
+ export type TopNavAppearance = 'transparent' | 'subtle' | 'subtle-circular' | 'filled-circular';
7
+ /** An item is a button by default, and an anchor when it goes somewhere. */
8
+ export type TopNavItemGenerics = 'button' | 'a';
9
+ export type TopNavItemElementDOMType = {
10
+ button: HTMLButtonElement;
11
+ a: HTMLAnchorElement;
12
+ };
13
+ export type TopNavContext = FSContext<{
14
+ appearance: TopNavAppearance;
15
+ size: Sizes;
16
+ vertical: boolean;
17
+ disabled: boolean;
18
+ selectTabOnFocus: boolean;
19
+ reserveSelectedTabSpace: boolean;
20
+ }, {
21
+ selectedValue: string;
22
+ }, null, {
23
+ selectTab: (event: Event, value: string) => void;
24
+ }>;
25
+ /** @propsmith TopNavProps */
26
+ export type TopNavProps = {
27
+ /** The DOM reference of the strip.
28
+ * @bindable
29
+ */
30
+ ref?: HTMLDivElement;
31
+ /** The value of the selected item.
32
+ * @bindable
33
+ */
34
+ selectedValue?: string;
35
+ /** Fired when an item is selected. */
36
+ onTabSelect?: (event: Event, value: string) => void;
37
+ /** How the strip is painted: `transparent` has no background of its own, `subtle` fills an item
38
+ * while it is engaged, and the two circular appearances trade the moving bar for a pill.
39
+ * @default 'transparent'
40
+ */
41
+ appearance?: TopNavAppearance;
42
+ /** The size of every item of the strip.
43
+ * @default 'medium'
44
+ */
45
+ size?: Sizes;
46
+ /** Arranges the items in a column, with the bar down their leading edge.
47
+ * @default false
48
+ */
49
+ vertical?: boolean;
50
+ /** Disables the user interaction on every item.
51
+ * @default false
52
+ */
53
+ disabled?: boolean;
54
+ /** Selects an item as soon as the focus reaches it, rather than waiting to be asked.
55
+ * @default false
56
+ */
57
+ selectTabOnFocus?: boolean;
58
+ /** A selected item sets its label in semibold, which is wider than the same label at rest. This
59
+ * holds that width from the start, so selecting an item never shifts the ones beside it.
60
+ * @default true
61
+ */
62
+ reserveSelectedTabSpace?: boolean;
63
+ /** Opts the strip out of the tabspot focus management, to wire your own.
64
+ * @default false
65
+ */
66
+ disableTabspot?: boolean;
67
+ } & HTMLAttributes<HTMLDivElement>;
68
+ /** @propsmith TopNavItemProps */
69
+ export type TopNavItemProps<T extends TopNavItemGenerics = 'button'> = {
70
+ /** The DOM reference of the item.
71
+ * @type TopNavItemElementDOMType[T]
72
+ * @bindable
73
+ */
74
+ ref?: TopNavItemElementDOMType[T];
75
+ /** The DOM element to render. An anchor takes the attributes of one, `href` among them, and is
76
+ * what an item that navigates should be.
77
+ * @type 'button' | 'a'
78
+ * @default 'button'
79
+ */
80
+ as?: T;
81
+ /** The value that identifies the item. Defaults to a generated id. */
82
+ value?: string;
83
+ /** The icon to display before the label.
84
+ * @type Snippet | Component
85
+ */
86
+ icon?: Snippet | Component;
87
+ /** Disables the user interaction. */
88
+ disabled?: boolean;
89
+ } & PolymorphicProps<T>;
@@ -0,0 +1 @@
1
+ export {};
@@ -35,7 +35,7 @@ Nesting a TreeView inside a TreeViewItem of type='branch' creates a subtree. Use
35
35
 
36
36
  ### Checkable Items
37
37
 
38
- Set `selectionMode="multiple"` to render checkboxes on selectable items.
38
+ Set `selectionMode` to `"multiple" | "single"` to render checkboxes on selectable items.
39
39
 
40
40
  ```svelte
41
41
  <TreeView selectionMode="multiple">
@@ -50,6 +50,132 @@ Set `selectionMode="multiple"` to render checkboxes on selectable items.
50
50
  </TreeView>
51
51
  ```
52
52
 
53
+ ### Single selection
54
+
55
+ Set `selectionMode` to `single` so checking an item unchecks the previous one, and branches no longer cascade to their children.
56
+
57
+ ```svelte
58
+ <script>
59
+ let checked = $state([]);
60
+ </script>
61
+
62
+ <TreeView selectionMode="single" bind:checkedItems={checked}>
63
+ <TreeViewItem id="fruits" type="branch" open>
64
+ <TreeViewItemContent>Fruits</TreeViewItemContent>
65
+ <TreeView>
66
+ <TreeViewItem id="apple">
67
+ <TreeViewItemContent>Apple</TreeViewItemContent>
68
+ </TreeViewItem>
69
+ <TreeViewItem id="banana">
70
+ <TreeViewItemContent>Banana</TreeViewItemContent>
71
+ </TreeViewItem>
72
+ </TreeView>
73
+ </TreeViewItem>
74
+ <TreeViewItem id="vegetables">
75
+ <TreeViewItemContent>Vegetables</TreeViewItemContent>
76
+ </TreeViewItem>
77
+ </TreeView>
78
+
79
+ <p>Checked: {checked.join(', ')}</p>
80
+ ```
81
+
82
+ ### Icons
83
+
84
+ Use `iconBefore` and `iconAfter` to render icons around the label. Every item reserves the space of the expand chevron, so checkboxes and icons stay aligned between branches and leaves.
85
+
86
+ ```svelte
87
+ <script>
88
+ import { TreeView, TreeViewItem, TreeViewItemContent } from 'fluentui-svelte';
89
+ import FolderRegular from 'fluentui-icons-svelte/FolderRegular.svelte';
90
+ import DocumentRegular from 'fluentui-icons-svelte/DocumentRegular.svelte';
91
+ import ImageRegular from 'fluentui-icons-svelte/ImageRegular.svelte';
92
+ import StarRegular from 'fluentui-icons-svelte/StarRegular.svelte';
93
+ import LockClosedRegular from 'fluentui-icons-svelte/LockClosedRegular.svelte';
94
+ </script>
95
+
96
+ {#snippet folder()}<FolderRegular width="1.25rem" />{/snippet}
97
+ {#snippet document()}<DocumentRegular width="1.25rem" />{/snippet}
98
+ {#snippet image()}<ImageRegular width="1.25rem" />{/snippet}
99
+ {#snippet star()}<StarRegular width="1rem" />{/snippet}
100
+ {#snippet lock()}<LockClosedRegular width="1rem" />{/snippet}
101
+
102
+ <TreeView selectionMode="multiple">
103
+ <TreeViewItem id="documents" type="branch" open>
104
+ <TreeViewItemContent iconBefore={folder}>Documents</TreeViewItemContent>
105
+ <TreeView>
106
+ <TreeViewItem id="report">
107
+ <TreeViewItemContent iconBefore={document} iconAfter={star}>Report</TreeViewItemContent>
108
+ </TreeViewItem>
109
+ <TreeViewItem id="photo">
110
+ <TreeViewItemContent iconBefore={image}>Photo</TreeViewItemContent>
111
+ </TreeViewItem>
112
+ </TreeView>
113
+ </TreeViewItem>
114
+ <TreeViewItem id="notes">
115
+ <TreeViewItemContent iconBefore={document} iconAfter={lock}>Notes</TreeViewItemContent>
116
+ </TreeViewItem>
117
+ </TreeView>
118
+ ```
119
+
120
+ ### Aside and actions
121
+
122
+ `aside` is pinned to the end of the row. `actions` comes after it and is revealed when the row is hovered or focused.
123
+
124
+ ```svelte
125
+ <script>
126
+ import { TreeView, TreeViewItem, TreeViewItemContent, Button } from 'fluentui-svelte';
127
+ import DocumentRegular from 'fluentui-icons-svelte/DocumentRegular.svelte';
128
+ import MoreHorizontalRegular from 'fluentui-icons-svelte/MoreHorizontalRegular.svelte';
129
+ </script>
130
+
131
+ {#snippet document()}<DocumentRegular width="1.25rem" />{/snippet}
132
+ {#snippet fileSize()}<span>2 KB</span>{/snippet}
133
+ {#snippet more()}
134
+ <Button appearance="subtle" onclick={(e) => e.stopPropagation()} aria-label="More actions"
135
+ ><MoreHorizontalRegular width="1rem" /></Button
136
+ >
137
+ {/snippet}
138
+
139
+ <TreeView>
140
+ <TreeViewItem id="report">
141
+ <TreeViewItemContent iconBefore={document} aside={fileSize} actions={more}>Report</TreeViewItemContent>
142
+ </TreeViewItem>
143
+ <TreeViewItem id="annual-summary">
144
+ <TreeViewItemContent iconBefore={document} aside={fileSize} actions={more}>Annual summary</TreeViewItemContent>
145
+ </TreeViewItem>
146
+ </TreeView>
147
+ ```
148
+
149
+ ### Sizes
150
+
151
+ Set `size` to `small`, `medium` or `large` on the root `TreeView`.
152
+
153
+ ```svelte
154
+ <TreeView size="small">...</TreeView>
155
+ <TreeView size="medium">...</TreeView>
156
+ <TreeView size="large">...</TreeView>
157
+ ```
158
+
159
+ ### Disabled items
160
+
161
+ A disabled item can't be opened or checked, and it is skipped when its parent checks its children, which leaves the parent in its mixed state.
162
+
163
+ ```svelte
164
+ <TreeView selectionMode="multiple">
165
+ <TreeViewItem id="fruits" type="branch" open>
166
+ <TreeViewItemContent>Fruits</TreeViewItemContent>
167
+ <TreeView>
168
+ <TreeViewItem id="apple">
169
+ <TreeViewItemContent>Apple</TreeViewItemContent>
170
+ </TreeViewItem>
171
+ <TreeViewItem id="banana" disabled>
172
+ <TreeViewItemContent>Banana</TreeViewItemContent>
173
+ </TreeViewItem>
174
+ </TreeView>
175
+ </TreeViewItem>
176
+ </TreeView>
177
+ ```
178
+
53
179
  ### Controlling Open and Checked State
54
180
 
55
181
  Use the bindable `openItems` and `checkedItems` props on the root `TreeView` to control state programmatically.
@@ -70,6 +196,45 @@ Use the bindable `openItems` and `checkedItems` props on the root `TreeView` to
70
196
  </TreeView>
71
197
  </TreeViewItem>
72
198
  </TreeView>
199
+
200
+ <p>Open: {openItems.join(', ')}</p>
201
+ <p>Checked: {checkedItems.join(', ')}</p>
202
+ ```
203
+
204
+ ### Events
205
+
206
+ `onCheckedChange` and `onOpenChange` on the TreeView receive the resulting ids. The same props on a TreeViewItem receive the item and its new state, including the children a branch cascades to.
207
+
208
+ ```svelte
209
+ <script>
210
+ let treeEvent = $state('');
211
+ let itemEvent = $state('');
212
+ </script>
213
+
214
+ <TreeView
215
+ selectionMode="multiple"
216
+ onCheckedChange={(e, checkedItems) => (treeEvent = 'Checked: ' + checkedItems.join(', '))}
217
+ onOpenChange={(e, openItems) => (treeEvent = 'Open: ' + openItems.join(', '))}
218
+ >
219
+ <TreeViewItem
220
+ id="fruits"
221
+ type="branch"
222
+ onOpenChange={(e, { id, open }) => (itemEvent = id + (open ? ' opened' : ' closed'))}
223
+ >
224
+ <TreeViewItemContent>Fruits</TreeViewItemContent>
225
+ <TreeView>
226
+ <TreeViewItem
227
+ id="apple"
228
+ onCheckedChange={(e, { id, checked }) => (itemEvent = id + (checked ? ' checked' : ' unchecked'))}
229
+ >
230
+ <TreeViewItemContent>Apple</TreeViewItemContent>
231
+ </TreeViewItem>
232
+ </TreeView>
233
+ </TreeViewItem>
234
+ </TreeView>
235
+
236
+ <p>Tree: {treeEvent}</p>
237
+ <p>Item: {itemEvent}</p>
73
238
  ```
74
239
 
75
240
  ## Component Props (TreeView)
@@ -47,10 +47,8 @@
47
47
  wrapperAttributes={{
48
48
  onclick: (e) => {
49
49
  e.stopPropagation();
50
- if (ITEM_CONTEXT.disabled) {
51
- e.preventDefault();
52
- return;
53
- }
50
+ e.preventDefault();
51
+ if (ITEM_CONTEXT.disabled) return;
54
52
  handleCheck(e, ITEM_CONTEXT.id, !ITEM_CONTEXT.checked);
55
53
  }
56
54
  }}
@@ -87,9 +85,12 @@
87
85
 
88
86
  <style>
89
87
  .fs-tree-view-item-content {
88
+ --chevron-width: 1.2rem;
89
+ --gap: 0.75rem;
90
+ box-sizing: border-box;
90
91
  display: flex;
91
92
  align-items: center;
92
- gap: 0.75rem;
93
+ gap: var(--gap);
93
94
  position: relative;
94
95
  border-radius: var(--fs-control-border-radius);
95
96
  font-size: var(--fs-body-font-size);
@@ -100,20 +101,26 @@
100
101
  width: 100%;
101
102
  text-align: left;
102
103
  user-select: none;
104
+ &:not(:has(:global(.branch-indicator))) {
105
+ --leaf-offset: calc(var(--chevron-width) + var(--gap) + 0rem);
106
+ }
103
107
  &.size-small {
104
108
  font-size: var(--fs-body2-font-size);
105
109
  padding: 0.125rem 0.5rem;
106
- padding-left: calc(0.5rem + 1.5rem * var(--depth, 0));
110
+ padding-left: calc(0.5rem + 2rem * var(--depth, 0) + var(--leaf-offset, 0rem));
111
+ min-height: 1.75rem;
107
112
  }
108
113
  &.size-medium {
109
114
  font-size: var(--fs-body-font-size);
110
115
  padding: 0.25rem 0.625rem;
111
- padding-left: calc(0.625rem + 1.5rem * var(--depth, 0));
116
+ padding-left: calc(0.625rem + 2rem * var(--depth, 0) + var(--leaf-offset, 0rem));
117
+ min-height: 2rem;
112
118
  }
113
119
  &.size-large {
114
120
  font-size: var(--fs-subtitle2-font-size);
115
121
  padding: 0.375rem 0.75rem;
116
- padding-left: calc(0.75rem + 1.5rem * var(--depth, 0));
122
+ padding-left: calc(0.75rem + 2rem * var(--depth, 0) + var(--leaf-offset, 0rem));
123
+ min-height: 2.25rem;
117
124
  }
118
125
 
119
126
  &::after {
@@ -155,4 +162,30 @@
155
162
  fill: currentColor;
156
163
  }
157
164
  }
165
+ .fs-tree-view-item-icon-before,
166
+ .fs-tree-view-item-icon-after,
167
+ .fs-tree-view-item-aside,
168
+ .fs-tree-view-item-actions {
169
+ display: inline-flex;
170
+ align-items: center;
171
+ flex-shrink: 0;
172
+ }
173
+ .fs-tree-view-item-aside,
174
+ .fs-tree-view-item-actions {
175
+ margin-left: auto;
176
+ }
177
+ .fs-tree-view-item-aside + .fs-tree-view-item-actions {
178
+ margin-left: 0;
179
+ }
180
+ .fs-tree-view-item-actions {
181
+ opacity: 0;
182
+ @media (hover: none) {
183
+ opacity: 1;
184
+ }
185
+ }
186
+ .fs-tree-view-item-content:hover .fs-tree-view-item-actions,
187
+ .fs-tree-view-item-content:focus-within .fs-tree-view-item-actions,
188
+ :global(.fs-tree-view-item:focus-visible) > .fs-tree-view-item-content .fs-tree-view-item-actions {
189
+ opacity: 1;
190
+ }
158
191
  </style>