@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 CHANGED
@@ -113,7 +113,7 @@ bun add @rokkit/ui
113
113
  - @rokkit/actions
114
114
  - @rokkit/core
115
115
  - @rokkit/data
116
- - @rokkit/input
116
+ - @rokkit/forms
117
117
  - @rokkit/states
118
118
  - d3-scale
119
119
  - date-fns
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.121",
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/input": "latest",
37
+ "@rokkit/forms": "latest",
38
38
  "@rokkit/states": "latest",
39
39
  "d3-scale": "^4.0.2",
40
40
  "date-fns": "^4.1.0",
@@ -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, crumb } = $props()
15
+ let { class: classes = '', items = [], separator = '/', fields, child } = $props()
16
+ let childSnippet = $derived(child ? child : defaultChild)
16
17
  </script>
17
18
 
18
- <rk-crumbs class={classes}>
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
- <span>
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
- <rk-crumb class:is-selected={index === items.length - 1}>
30
- {#if crumb}
31
- {@render crumb(item, fields)}
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
- </rk-crumbs>
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
- class:primary
36
- class:secondary
37
- class:tertiary
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(null),
32
- ...events
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
- emitter?.change(checked)
44
+ checked = !Boolean(checked)
45
+ onchange?.(checked)
44
46
  }
45
- emitter?.click()
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
- <rk-icon
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={emitter.mouseenter}
73
- onnmouseleave={emitter.nmouseleave}
72
+ {onmouseenter}
73
+ {onmouseleave}
74
+ {...restProps}
74
75
  >
75
- <i class={name} aria-hidden="true"></i>
76
- </rk-icon>
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 proxy = $derived(new Proxy(value, fields))
12
- let content = $derived(proxy.get('text'))
13
- let ariaLabel = $derived(proxy.get('label') ?? content)
14
- let icon = $derived(proxy.get('icon'))
15
- let image = $derived(proxy.get('image'))
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
- <rk-list
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
- <rk-header>{@render header()}</rk-header>
65
+ <div data-list-header>{@render header()}</div>
65
66
  {/if}
66
- <rk-body>
67
+ <div data-list-body>
67
68
  {#if items.length === 0}
68
69
  {#if empty}
69
70
  {@render empty()}
70
71
  {:else}
71
- <rk-message>No items found.</rk-message>
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
- </rk-body>
85
+ </div>
85
86
  {#if footer}
86
- <rk-footer>{@render footer()}</rk-footer>
87
+ <div data-list-footer>{@render footer()}</div>
87
88
  {/if}
88
- </rk-list>
89
+ </div>
@@ -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
- <rk-list-item
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
- </rk-list-item>
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
- <rk-pill use:keyboard={keyMappings} onremove={handle} tabindex="0" class={classes}>
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
- </rk-pill>
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} [class]
11
- * @property {any} value
12
- * @property {Array<any>} [options]
13
- * @property {FieldMapper} [mapping]
14
- * @property {boolean} [compact]
15
- * @property {boolean} [disabled]
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 = noop,
27
- stub,
26
+ onchange,
27
+ child,
28
28
  ...extra
29
29
  } = $props()
30
30
 
31
- // let cursor = $state([])
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
- // let useComponent = $derived(!options.every((item) => [false, true].includes(item)))
56
- // let mapper = new FieldMapper(fields)
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
- <rk-error>Items should be an array with at least two items.</rk-error>
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
- <rk-switch
73
+ <div
64
74
  class={classes}
65
- class:is-off={options.length === 2 && equals(value, options[0])}
66
- class:is-on={options.length === 2 && equals(value, options[1])}
67
- class:compact
68
- aria-label="Toggle Switch"
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 (item)}
79
- <!-- {@const Template = getSnippet(extra, mapper.get('snippet', item), stub)} -->
80
- <rk-item class="relative" role="option" aria-selected={equals(item, value)} data-path={index}>
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
- <rk-indicator class="absolute bottom-0 left-0 right-0 top-0"></rk-indicator>
99
+ <div data-switch-mark class="absolute bottom-0 left-0 right-0 top-0"></div>
83
100
  {/if}
84
- {#if stub}
85
- {@render stub(item, fields)}
86
- <!-- <Template value={item} {fields} /> -->
87
- {:else}
88
- <Item value={item} {fields} />
89
- {/if}
90
- </rk-item>
101
+
102
+ {@render childSnippet?.(proxy)}
103
+ </div>
91
104
  {/each}
92
- </rk-switch>
105
+ </div>
93
106
  {/if}
package/src/Tabs.svelte CHANGED
@@ -1,96 +1,166 @@
1
1
  <script>
2
- import { defaultFields, defaultStateIcons, noop, getSnippet, FieldMapper } from '@rokkit/core'
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} Props
10
- * @property {string} [class]
11
- * @property {any} [options]
12
- * @property {import('@rokkit/core').FieldMapping} [fields]
13
- * @property {any} [value]
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
- /** @type {Props} */
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: className = '',
23
- options = $bindable([]),
24
- value = $bindable(null),
25
- icons = $bindable(defaultStateIcons.action),
26
- fields = defaultFields,
27
- below = false,
28
- align = 'left',
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
- onremove = noop,
31
- onadd = noop,
32
- onselect = noop,
33
- stub,
34
- ...extra
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
- function handleRemove(event) {
38
- if (typeof event.detail === Object) {
39
- event.detail[fields.isDeleted] = true
40
- } else {
41
- options = options.filter((i) => i !== event.detail)
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
- onremove({ item: event.detail })
75
+ if (has(name, emitter)) {
76
+ value = data.value
77
+ emitter[name](data)
78
+ }
45
79
  }
46
- function handleAdd(event) {
47
- event.stopPropagation()
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
- onselect({ item: value, indices: cursor })
85
+ function handleRemove(item) {
86
+ onremove?.(item)
55
87
  }
56
- let stateIcons = $derived({ ...defaultStateIcons.action, ...icons })
57
- let filtered = $derived(options.filter((item) => !item[fields.deleted]))
58
- let wrapper = $derived(new ListController(options, value, fields))
59
- let mapper = new FieldMapper(fields)
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
- <rk-tabs
63
- class="flex w-full {className}"
64
- class:is-below={below}
65
- class:justify-center={align === 'center'}
66
- class:justify-end={align === 'right'}
67
- tabindex="0"
68
- role="listbox"
69
- use:navigator={{ wrapper, horizontal: true }}
70
- onaction={handleNav}
71
- onremove={handleRemove}
72
- onadd={handleAdd}
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
- {#each filtered as item, index (index)}
75
- {@const Template = getSnippet(extra, mapper.get('snippet', item), stub)}
76
- <rk-tab>
77
- {#if Template}
78
- <Template value={item} {fields} />
79
- {:else}
80
- <Item value={item} {fields} />
81
- {/if}
82
- {#if editable}
83
- <Icon
84
- name="remove"
85
- role="button"
86
- label="Delete Tab"
87
- size="small"
88
- onclick={() => handleRemove(item)}
89
- />
90
- {/if}
91
- </rk-tab>
92
- {/each}
93
- {#if editable}
94
- <Icon name="add" role="button" label="Add Tab" size="small" onclick={handleAdd} />
95
- {/if}
96
- </rk-tabs>
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>