@rokkit/ui 1.0.0-next.121 → 1.0.0-next.122
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/README.md +1 -1
- package/dist/index.d.ts +0 -5
- package/dist/tree/List.spec.svelte.d.ts +1 -0
- package/dist/tree/Node.spec.svelte.d.ts +1 -0
- package/dist/tree/Root.spec.svelte.d.ts +1 -0
- package/package.json +2 -2
- package/src/BreadCrumbs.svelte +14 -19
- package/src/Button.svelte +3 -7
- package/src/Icon.svelte +21 -16
- package/src/Item.svelte +6 -6
- package/src/List.svelte +8 -7
- package/src/ListBody.svelte +3 -2
- package/src/Pill.svelte +2 -2
- package/src/Select.svelte +0 -3
- package/src/Switch.svelte +45 -32
- package/src/Tabs.svelte +147 -77
- package/src/TabsOld.svelte +96 -0
- package/src/Toggle.svelte +2 -1
- package/src/ToggleThemeMode.svelte +7 -3
- package/src/index.js +0 -5
- package/src/tree/List.spec.svelte.js +84 -0
- package/src/tree/List.svelte +78 -0
- package/src/tree/Node.spec.svelte.js +104 -0
- package/src/tree/Node.svelte +80 -0
- package/src/tree/Root.spec.svelte.js +63 -0
- package/src/tree/Root.svelte +81 -0
- package/dist/input/types.d.ts +0 -9
- package/src/DataEditor.svelte +0 -31
- package/src/FieldLayout.svelte +0 -48
- package/src/Form.svelte +0 -17
- package/src/ListEditor.svelte +0 -44
- package/src/NestedEditor.svelte +0 -88
- package/src/input/Input.svelte +0 -17
- package/src/input/InputField.svelte +0 -69
- package/src/input/InputSelect.svelte +0 -23
- package/src/input/InputSwitch.svelte +0 -19
- package/src/input/types.js +0 -29
package/README.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -33,11 +33,6 @@ export { default as ToggleThemeMode } from "./ToggleThemeMode.svelte";
|
|
|
33
33
|
export { default as Overlay } from "./Overlay.svelte";
|
|
34
34
|
export { default as Message } from "./Message.svelte";
|
|
35
35
|
export { default as SlidingColumns } from "./SlidingColumns.svelte";
|
|
36
|
-
export { default as InputField } from "./input/InputField.svelte";
|
|
37
|
-
export { default as Form } from "./Form.svelte";
|
|
38
|
-
export { default as FieldLayout } from "./FieldLayout.svelte";
|
|
39
|
-
export { default as DataEditor } from "./DataEditor.svelte";
|
|
40
|
-
export { default as NestedEditor } from "./NestedEditor.svelte";
|
|
41
36
|
export { default as Stepper } from "./Stepper.svelte";
|
|
42
37
|
export { default as ProgressDots } from "./ProgressDots.svelte";
|
|
43
38
|
export { default as Card } from "./Card.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rokkit/ui",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.122",
|
|
4
4
|
"description": "Data driven UI components, improving DX",
|
|
5
5
|
"author": "Jerry Thomas <me@jerrythomas.name>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"@rokkit/actions": "latest",
|
|
35
35
|
"@rokkit/core": "latest",
|
|
36
36
|
"@rokkit/data": "latest",
|
|
37
|
-
"@rokkit/
|
|
37
|
+
"@rokkit/forms": "latest",
|
|
38
38
|
"@rokkit/states": "latest",
|
|
39
39
|
"d3-scale": "^4.0.2",
|
|
40
40
|
"date-fns": "^4.1.0",
|
package/src/BreadCrumbs.svelte
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { getSnippet } from '@rokkit/core'
|
|
3
2
|
import Item from './Item.svelte'
|
|
4
|
-
|
|
3
|
+
import Icon from './Icon.svelte'
|
|
4
|
+
import { Proxy } from '@rokkit/states'
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} Props
|
|
7
7
|
* @property {string} [class]
|
|
@@ -12,26 +12,21 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
/** @type {Props} */
|
|
15
|
-
let { class: classes = '', items = [], separator = '/', fields,
|
|
15
|
+
let { class: classes = '', items = [], separator = '/', fields, child } = $props()
|
|
16
|
+
let childSnippet = $derived(child ? child : defaultChild)
|
|
16
17
|
</script>
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
{#snippet defaultChild(proxy)}
|
|
20
|
+
<Item {proxy} />
|
|
21
|
+
{/snippet}
|
|
22
|
+
<div data-crumb-root class={classes}>
|
|
19
23
|
{#each items as item, index (index)}
|
|
24
|
+
{@const proxy = new Proxy(item, fields)}
|
|
20
25
|
{#if index > 0}
|
|
21
|
-
<
|
|
22
|
-
{#if separator.length === 1}
|
|
23
|
-
{separator}
|
|
24
|
-
{:else}
|
|
25
|
-
<icon class={separator}></icon>
|
|
26
|
-
{/if}
|
|
27
|
-
</span>
|
|
26
|
+
<Icon name={separator} data-crumb-separator></Icon>
|
|
28
27
|
{/if}
|
|
29
|
-
<
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
{:else}
|
|
33
|
-
<Item value={item} {fields} />
|
|
34
|
-
{/if}
|
|
35
|
-
</rk-crumb>
|
|
28
|
+
<div data-crumb-item class:is-selected={index === items.length - 1}>
|
|
29
|
+
{@render childSnippet?.(proxy)}
|
|
30
|
+
</div>
|
|
36
31
|
{/each}
|
|
37
|
-
</
|
|
32
|
+
</div>
|
package/src/Button.svelte
CHANGED
|
@@ -25,16 +25,12 @@
|
|
|
25
25
|
disabled = false,
|
|
26
26
|
onclick
|
|
27
27
|
} = $props()
|
|
28
|
-
|
|
29
|
-
const primary = $derived(variant === 'primary')
|
|
30
|
-
const secondary = $derived(variant === 'secondary')
|
|
31
|
-
const tertiary = $derived(variant === 'tertiary')
|
|
32
28
|
</script>
|
|
33
29
|
|
|
34
30
|
<button
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
data-button-root
|
|
32
|
+
data-variant={variant}
|
|
33
|
+
data-disabled={disabled}
|
|
38
34
|
class={classes}
|
|
39
35
|
{disabled}
|
|
40
36
|
{type}
|
package/src/Icon.svelte
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import { createEmitter } from '@rokkit/core'
|
|
3
2
|
/**
|
|
4
3
|
* @typedef {Object} Props
|
|
5
4
|
* @property {string} [class]
|
|
@@ -21,40 +20,41 @@
|
|
|
21
20
|
let {
|
|
22
21
|
ref = $bindable(),
|
|
23
22
|
class: classes = '',
|
|
24
|
-
name,
|
|
23
|
+
name = '?',
|
|
25
24
|
state = null,
|
|
26
25
|
size = 'base',
|
|
27
26
|
role = 'img',
|
|
28
27
|
label = null,
|
|
29
28
|
disabled = false,
|
|
30
29
|
tabindex = $bindable(0),
|
|
31
|
-
checked = $bindable(
|
|
32
|
-
|
|
30
|
+
checked = $bindable(),
|
|
31
|
+
onclick,
|
|
32
|
+
onchange,
|
|
33
|
+
onmouseenter,
|
|
34
|
+
onmouseleave,
|
|
35
|
+
...restProps
|
|
33
36
|
} = $props()
|
|
34
37
|
|
|
35
|
-
let emitter = $derived(createEmitter(events, ['click', 'change', 'mouseenter', 'mouseleave']))
|
|
36
38
|
function handleClick(e) {
|
|
37
39
|
if (role === 'img') return
|
|
38
40
|
e.preventDefault()
|
|
39
41
|
|
|
40
42
|
if (!disabled) {
|
|
41
43
|
if (isCheckbox) {
|
|
42
|
-
checked = !checked
|
|
43
|
-
|
|
44
|
+
checked = !Boolean(checked)
|
|
45
|
+
onchange?.(checked)
|
|
44
46
|
}
|
|
45
|
-
|
|
47
|
+
onclick?.()
|
|
46
48
|
}
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
let isCheckbox = $derived(role === 'checkbox' || role === 'option')
|
|
50
52
|
let validatedTabindex = $derived(role === 'img' || disabled ? -1 : tabindex)
|
|
51
|
-
let ariaChecked = $derived(
|
|
52
|
-
['checkbox', 'option'].includes(role) ? (checked !== null ? checked : false) : null
|
|
53
|
-
)
|
|
53
|
+
let ariaChecked = $derived(['checkbox', 'option'].includes(role) && checked)
|
|
54
54
|
</script>
|
|
55
55
|
|
|
56
56
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
57
|
-
<
|
|
57
|
+
<icon
|
|
58
58
|
bind:this={ref}
|
|
59
59
|
data-tag-icon
|
|
60
60
|
data-state={state}
|
|
@@ -69,8 +69,13 @@
|
|
|
69
69
|
onclick={handleClick}
|
|
70
70
|
onkeydown={(e) => e.key === 'Enter' && e.currentTarget.click()}
|
|
71
71
|
tabindex={validatedTabindex}
|
|
72
|
-
onmouseenter
|
|
73
|
-
|
|
72
|
+
{onmouseenter}
|
|
73
|
+
{onmouseleave}
|
|
74
|
+
{...restProps}
|
|
74
75
|
>
|
|
75
|
-
|
|
76
|
-
</
|
|
76
|
+
{#if name.length <= 2}
|
|
77
|
+
<span>{name}</span>
|
|
78
|
+
{:else}
|
|
79
|
+
<i class={name} aria-hidden="true"></i>
|
|
80
|
+
{/if}
|
|
81
|
+
</icon>
|
package/src/Item.svelte
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
/** @type {Props} */
|
|
10
|
-
let { value, fields } = $props()
|
|
11
|
-
let
|
|
12
|
-
let content = $derived(
|
|
13
|
-
let ariaLabel = $derived(
|
|
14
|
-
let icon = $derived(
|
|
15
|
-
let image = $derived(
|
|
10
|
+
let { value, fields, proxy = null } = $props()
|
|
11
|
+
let proxyItem = $derived(proxy ?? new Proxy(value, fields))
|
|
12
|
+
let content = $derived(proxyItem.get('text'))
|
|
13
|
+
let ariaLabel = $derived(proxyItem.get('label') ?? content)
|
|
14
|
+
let icon = $derived(proxyItem.get('icon'))
|
|
15
|
+
let image = $derived(proxyItem.get('image'))
|
|
16
16
|
</script>
|
|
17
17
|
|
|
18
18
|
{#if icon}
|
package/src/List.svelte
CHANGED
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
let wrapper = new ListController(items, value, fields, { multiSelect })
|
|
53
53
|
</script>
|
|
54
54
|
|
|
55
|
-
<
|
|
55
|
+
<div
|
|
56
|
+
data-list
|
|
56
57
|
class={classes}
|
|
57
58
|
role="listbox"
|
|
58
59
|
aria-label={name}
|
|
@@ -61,14 +62,14 @@
|
|
|
61
62
|
onaction={handleAction}
|
|
62
63
|
>
|
|
63
64
|
{#if header}
|
|
64
|
-
<
|
|
65
|
+
<div data-list-header>{@render header()}</div>
|
|
65
66
|
{/if}
|
|
66
|
-
<
|
|
67
|
+
<div data-list-body>
|
|
67
68
|
{#if items.length === 0}
|
|
68
69
|
{#if empty}
|
|
69
70
|
{@render empty()}
|
|
70
71
|
{:else}
|
|
71
|
-
<
|
|
72
|
+
<p>No items found.</p>
|
|
72
73
|
{/if}
|
|
73
74
|
{:else}
|
|
74
75
|
<ListBody
|
|
@@ -81,8 +82,8 @@
|
|
|
81
82
|
{snippets}
|
|
82
83
|
/>
|
|
83
84
|
{/if}
|
|
84
|
-
</
|
|
85
|
+
</div>
|
|
85
86
|
{#if footer}
|
|
86
|
-
<
|
|
87
|
+
<div data-list-footer>{@render footer()}</div>
|
|
87
88
|
{/if}
|
|
88
|
-
</
|
|
89
|
+
</div>
|
package/src/ListBody.svelte
CHANGED
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
{@const template = getSnippet(snippets, fm.get('snippet', item, 'stub'))}
|
|
23
23
|
{@const pathKey = getKeyFromPath([...path, index])}
|
|
24
24
|
{@const props = fm.get('props', item) || {}}
|
|
25
|
-
<
|
|
25
|
+
<div
|
|
26
|
+
data-list-item
|
|
26
27
|
role="option"
|
|
27
28
|
data-path={pathKey}
|
|
28
29
|
aria-selected={selectedKeys.has(pathKey)}
|
|
@@ -38,5 +39,5 @@
|
|
|
38
39
|
<Item value={item} {fields} />
|
|
39
40
|
{/if}
|
|
40
41
|
</svelte:boundary>
|
|
41
|
-
</
|
|
42
|
+
</div>
|
|
42
43
|
{/each}
|
package/src/Pill.svelte
CHANGED
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
</script>
|
|
33
33
|
|
|
34
34
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
35
|
-
<
|
|
35
|
+
<div data-pill-root use:keyboard={keyMappings} onremove={handle} tabindex="0" class={classes}>
|
|
36
36
|
<Item {value} {mapping}></Item>
|
|
37
37
|
{#if removable}
|
|
38
38
|
<Icon name="action-close" role="button" aria-label="Remove" {disabled} onclick={handle} small
|
|
39
39
|
></Icon>
|
|
40
40
|
{/if}
|
|
41
|
-
</
|
|
41
|
+
</div>
|
package/src/Select.svelte
CHANGED
|
@@ -52,9 +52,6 @@
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// $: fields = { ...defaultFields, ...fields }
|
|
56
|
-
// $: using = { default: Item, ...using }
|
|
57
|
-
// $: activeIndex = options.findIndex((item) => item === value)
|
|
58
55
|
let offsetTop = $derived(activeItem?.offsetTop + activeItem?.clientHeight ?? 0)
|
|
59
56
|
</script>
|
|
60
57
|
|
package/src/Switch.svelte
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { equals } from 'ramda'
|
|
3
|
-
import { noop, FieldMapper } from '@rokkit/core'
|
|
4
3
|
import { keyboard } from '@rokkit/actions'
|
|
4
|
+
import { Proxy } from '@rokkit/states'
|
|
5
5
|
import Item from './Item.svelte'
|
|
6
|
-
// import { defaultMapping } from './constants'
|
|
7
6
|
|
|
8
7
|
/**
|
|
9
8
|
* @typedef {Object} Props
|
|
10
|
-
* @property {string}
|
|
11
|
-
* @property {any}
|
|
12
|
-
* @property {
|
|
13
|
-
* @property {
|
|
14
|
-
* @property {boolean}
|
|
15
|
-
* @property {boolean}
|
|
9
|
+
* @property {string} [class]
|
|
10
|
+
* @property {any} value
|
|
11
|
+
* @property {import('@rokkit/core').FieldMapping} fields
|
|
12
|
+
* @property {Array<any>} [options]
|
|
13
|
+
* @property {boolean} [compact]
|
|
14
|
+
* @property {boolean} [disabled]
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
/** @type {Props} */
|
|
19
18
|
let {
|
|
20
19
|
class: classes = '',
|
|
21
20
|
value = $bindable(),
|
|
21
|
+
description = 'Toggle Switch',
|
|
22
22
|
options = [false, true],
|
|
23
23
|
fields,
|
|
24
24
|
compact = false,
|
|
25
25
|
disabled = false,
|
|
26
|
-
onchange
|
|
27
|
-
|
|
26
|
+
onchange,
|
|
27
|
+
child,
|
|
28
28
|
...extra
|
|
29
29
|
} = $props()
|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Toggles the value of the switch
|
|
33
|
+
* @param {number} direction - The direction to toggle the switch
|
|
34
|
+
*/
|
|
33
35
|
function toggle(direction = 1) {
|
|
34
36
|
let nextIndex
|
|
35
37
|
const index = options.indexOf(value)
|
|
@@ -43,6 +45,10 @@
|
|
|
43
45
|
onchange(value)
|
|
44
46
|
}
|
|
45
47
|
|
|
48
|
+
/**
|
|
49
|
+
*
|
|
50
|
+
* @param event
|
|
51
|
+
*/
|
|
46
52
|
function handleClick(event) {
|
|
47
53
|
const index = event.target.closest('[data-path]').dataset.path
|
|
48
54
|
value = options[index]
|
|
@@ -52,20 +58,25 @@
|
|
|
52
58
|
next: ['ArrowRight', 'ArrowDown', ' ', 'Enter'],
|
|
53
59
|
prev: ['ArrowLeft', 'ArrowUp']
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
|
|
62
|
+
let childSnippet = $derived(child ? child : defaultChild)
|
|
57
63
|
</script>
|
|
58
64
|
|
|
65
|
+
{#snippet defaultChild(proxy)}
|
|
66
|
+
<Item {proxy} />
|
|
67
|
+
{/snippet}
|
|
68
|
+
|
|
59
69
|
{#if !Array.isArray(options) || options.length < 2}
|
|
60
|
-
<
|
|
70
|
+
<div data-error>Items should be an array with at least two items.</div>
|
|
61
71
|
{:else}
|
|
62
72
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
63
|
-
<
|
|
73
|
+
<div
|
|
64
74
|
class={classes}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
data-switch-root
|
|
76
|
+
data-switch-off={options.length === 2 && equals(value, options[0])}
|
|
77
|
+
data-switch-on={options.length === 2 && equals(value, options[1])}
|
|
78
|
+
data-switch-compact={compact}
|
|
79
|
+
aria-label={description}
|
|
69
80
|
aria-orientation="horizontal"
|
|
70
81
|
aria-disabled={disabled}
|
|
71
82
|
tabindex="0"
|
|
@@ -75,19 +86,21 @@
|
|
|
75
86
|
onprev={() => toggle(-1)}
|
|
76
87
|
onclick={handleClick}
|
|
77
88
|
>
|
|
78
|
-
{#each options as item, index (
|
|
79
|
-
|
|
80
|
-
<
|
|
89
|
+
{#each options as item, index (index)}
|
|
90
|
+
{@const proxy = new Proxy(item, fields)}
|
|
91
|
+
<div
|
|
92
|
+
data-switch-item
|
|
93
|
+
class="relative"
|
|
94
|
+
role="option"
|
|
95
|
+
aria-selected={equals(item, value)}
|
|
96
|
+
data-path={index}
|
|
97
|
+
>
|
|
81
98
|
{#if equals(item, value)}
|
|
82
|
-
<
|
|
99
|
+
<div data-switch-mark class="absolute bottom-0 left-0 right-0 top-0"></div>
|
|
83
100
|
{/if}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{:else}
|
|
88
|
-
<Item value={item} {fields} />
|
|
89
|
-
{/if}
|
|
90
|
-
</rk-item>
|
|
101
|
+
|
|
102
|
+
{@render childSnippet?.(proxy)}
|
|
103
|
+
</div>
|
|
91
104
|
{/each}
|
|
92
|
-
</
|
|
105
|
+
</div>
|
|
93
106
|
{/if}
|
package/src/Tabs.svelte
CHANGED
|
@@ -1,96 +1,166 @@
|
|
|
1
1
|
<script>
|
|
2
|
-
import {
|
|
3
|
-
import { ListController } from '@rokkit/states'
|
|
2
|
+
import { createEmitter, getKeyFromPath, defaultStateIcons } from '@rokkit/core'
|
|
4
3
|
import { navigator } from '@rokkit/actions'
|
|
4
|
+
import { ListController } from '@rokkit/states'
|
|
5
|
+
import { Proxy } from '@rokkit/states'
|
|
6
|
+
import { has, equals, pick } from 'ramda'
|
|
5
7
|
import Icon from './Icon.svelte'
|
|
6
|
-
import Item from './Item.svelte'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* @typedef {Object}
|
|
10
|
-
* @property {string} [
|
|
11
|
-
* @property {
|
|
12
|
-
* @property {
|
|
13
|
-
* @property {
|
|
14
|
-
* @property {boolean} [below]
|
|
15
|
-
* @property {string} [align]
|
|
16
|
-
* @property {boolean} [editable]
|
|
17
|
-
* @property {any} [icons]
|
|
10
|
+
* @typedef {Object} FieldMapping
|
|
11
|
+
* @property {string} [id] - Field to use for item ID
|
|
12
|
+
* @property {string} [label] - Field to use for item label/text
|
|
13
|
+
* @property {string} [value] - Field to use for item value
|
|
14
|
+
* @property {string} [content] - Field to use for tab content
|
|
18
15
|
*/
|
|
19
16
|
|
|
20
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} TabProps
|
|
19
|
+
* @property {string} [class] - Additional CSS class names
|
|
20
|
+
* @property {string} [name] - Name for accessibility
|
|
21
|
+
* @property {any[]} [items] - Array of tab items to display
|
|
22
|
+
* @property {FieldMapping} [fields] - Field mappings for extracting data
|
|
23
|
+
* @property {'horizontal'|'vertical'} [orientation] - Orientation of the tab bar
|
|
24
|
+
* @property {'before' | 'after' } [position] - Position of the tab bar
|
|
25
|
+
* @property {'start'|'center'|'end'} [align] - Alignment of the tab bar
|
|
26
|
+
* @property {any} [value] - Selected tab value (bindable)
|
|
27
|
+
* @property {number} [tabindex] - Tab index for keyboard navigation
|
|
28
|
+
* @property {boolean} [editable] - Whether tabs can be added/removed
|
|
29
|
+
* @property {string} [placeholder] - Placeholder text for input field
|
|
30
|
+
* @property {import('svelte').Snippet} [child] - Snippet for rendering tab headers
|
|
31
|
+
* @property {import('svelte').Snippet} [children] - Snippet for rendering tab content
|
|
32
|
+
* @property {import('svelte').Snippet} [empty] - Snippet for rendering empty state
|
|
33
|
+
* @property {Function} [onselect] - Callback when tab is selected
|
|
34
|
+
* @property {Function} [onchange] - Callback when tab changes
|
|
35
|
+
* @property {Function} [onmove] - Callback when focus moves
|
|
36
|
+
* @property {Function} [onadd] - Callback when tab is added
|
|
37
|
+
* @property {Function} [onremove] - Callback when tab is removed
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/** @type {TabProps} */
|
|
21
41
|
let {
|
|
22
|
-
class:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
align = '
|
|
42
|
+
class: classes = '',
|
|
43
|
+
name = 'tabs',
|
|
44
|
+
items = $bindable([]),
|
|
45
|
+
fields = {},
|
|
46
|
+
value = $bindable(),
|
|
47
|
+
orientation = 'horizontal',
|
|
48
|
+
align = 'start',
|
|
49
|
+
position = 'before',
|
|
50
|
+
tabindex = 0,
|
|
29
51
|
editable = false,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
child,
|
|
53
|
+
children,
|
|
54
|
+
empty,
|
|
55
|
+
placeholder = 'Select a tab to view its content.',
|
|
56
|
+
icons,
|
|
57
|
+
onselect,
|
|
58
|
+
onchange,
|
|
59
|
+
onmove,
|
|
60
|
+
onadd,
|
|
61
|
+
onremove,
|
|
62
|
+
...snippets
|
|
35
63
|
} = $props()
|
|
36
64
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
/** @type {Proxy[]} */
|
|
66
|
+
let proxyItems = $derived(items.map((item) => new Proxy(item, fields)))
|
|
67
|
+
let childSnippet = $derived(child ?? defaultChild)
|
|
68
|
+
let childrenSnippet = $derived(children ?? defaultChildren)
|
|
69
|
+
let emptyMessage = $derived(empty ?? defaultEmpty)
|
|
70
|
+
let activeItem = $derived(proxyItems.find((proxy) => equals(proxy.value, value)))
|
|
71
|
+
|
|
72
|
+
function handleAction(event) {
|
|
73
|
+
const { name, data } = event.detail
|
|
43
74
|
|
|
44
|
-
|
|
75
|
+
if (has(name, emitter)) {
|
|
76
|
+
value = data.value
|
|
77
|
+
emitter[name](data)
|
|
78
|
+
}
|
|
45
79
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
onadd()
|
|
80
|
+
|
|
81
|
+
function handleAdd() {
|
|
82
|
+
onadd?.()
|
|
49
83
|
}
|
|
50
|
-
function handleNav(event) {
|
|
51
|
-
value = event.detail.node
|
|
52
|
-
cursor = event.detail.path
|
|
53
84
|
|
|
54
|
-
|
|
85
|
+
function handleRemove(item) {
|
|
86
|
+
onremove?.(item)
|
|
55
87
|
}
|
|
56
|
-
let
|
|
57
|
-
let
|
|
58
|
-
let wrapper =
|
|
59
|
-
|
|
88
|
+
let tabIcons = $derived({ ...pick(['add', 'close'], defaultStateIcons.action), ...icons })
|
|
89
|
+
let emitter = createEmitter({ onchange, onmove, onselect }, ['select', 'change', 'move'])
|
|
90
|
+
let wrapper = new ListController(items, value, fields)
|
|
91
|
+
$effect(() => {
|
|
92
|
+
wrapper.update(items)
|
|
93
|
+
})
|
|
60
94
|
</script>
|
|
61
95
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
{#snippet defaultChild(item)}
|
|
97
|
+
{item.get('text') || item.get('label') || item.get('name')}
|
|
98
|
+
{/snippet}
|
|
99
|
+
|
|
100
|
+
{#snippet defaultChildren(item)}
|
|
101
|
+
<div data-tab-content-default>
|
|
102
|
+
{item.get('content')}
|
|
103
|
+
</div>
|
|
104
|
+
{/snippet}
|
|
105
|
+
|
|
106
|
+
{#snippet defaultEmpty()}
|
|
107
|
+
No tabs available.
|
|
108
|
+
{/snippet}
|
|
109
|
+
|
|
110
|
+
<div
|
|
111
|
+
data-tabs-root
|
|
112
|
+
data-orientation={orientation}
|
|
113
|
+
data-position={position}
|
|
114
|
+
data-align={align}
|
|
115
|
+
class={classes}
|
|
116
|
+
role="tablist"
|
|
117
|
+
aria-label={name}
|
|
118
|
+
use:navigator={{ wrapper, orientation }}
|
|
119
|
+
{tabindex}
|
|
120
|
+
onaction={handleAction}
|
|
73
121
|
>
|
|
74
|
-
|
|
75
|
-
{
|
|
76
|
-
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
<div data-tabs-list>
|
|
123
|
+
{#each proxyItems as item, index (index)}
|
|
124
|
+
{@const key = getKeyFromPath([index])}
|
|
125
|
+
{@const isSelected = equals(item.value, value)}
|
|
126
|
+
{@const isFocused = wrapper.focusedKey === key}
|
|
127
|
+
<div
|
|
128
|
+
data-tabs-trigger
|
|
129
|
+
data-path={getKeyFromPath([index])}
|
|
130
|
+
role="tab"
|
|
131
|
+
aria-selected={isSelected}
|
|
132
|
+
aria-controls="tab-panel-{index}"
|
|
133
|
+
class:selected={isSelected}
|
|
134
|
+
class:focused={isFocused}
|
|
135
|
+
>
|
|
136
|
+
{@render childSnippet(item)}
|
|
137
|
+
{#if editable}
|
|
138
|
+
<Icon
|
|
139
|
+
data-icon-remove
|
|
140
|
+
name={tabIcons.close}
|
|
141
|
+
role="button"
|
|
142
|
+
onclick={() => handleRemove(item.value)}
|
|
143
|
+
/>
|
|
144
|
+
{/if}
|
|
145
|
+
</div>
|
|
146
|
+
{/each}
|
|
147
|
+
{#if editable}
|
|
148
|
+
<Icon data-icon-add name={tabIcons.add} role="button" onclick={handleAdd} />
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Tab Content -->
|
|
153
|
+
<div data-tabs-content role="tabpanel">
|
|
154
|
+
{#if proxyItems.length === 0}
|
|
155
|
+
<div data-empty>
|
|
156
|
+
{@render emptyMessage()}
|
|
157
|
+
</div>
|
|
158
|
+
{:else if activeItem}
|
|
159
|
+
{@render childrenSnippet(activeItem)}
|
|
160
|
+
{:else}
|
|
161
|
+
<div data-placeholder>
|
|
162
|
+
{placeholder}
|
|
163
|
+
</div>
|
|
164
|
+
{/if}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|