@rokkit/ui 1.0.0-next.107 → 1.0.0-next.109

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -22,7 +22,6 @@ export { default as Toggle } from "./Toggle.svelte";
22
22
  export { default as Switch } from "./Switch.svelte";
23
23
  export { default as List } from "./List.svelte";
24
24
  export { default as Accordion } from "./Accordion.svelte";
25
- export { default as NestedList } from "./NestedList.svelte";
26
25
  export { default as Tree } from "./Tree.svelte";
27
26
  export { default as Tabs } from "./Tabs.svelte";
28
27
  export { default as Select } from "./Select.svelte";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rokkit/ui",
3
- "version": "1.0.0-next.107",
3
+ "version": "1.0.0-next.109",
4
4
  "description": "Organisms are larger, more complex building blocks that are composed of multiple molecules",
5
5
  "author": "Jerry Thomas <me@jerrythomas.name>",
6
6
  "license": "MIT",
@@ -32,6 +32,8 @@
32
32
  "@rokkit/actions": "latest",
33
33
  "@rokkit/core": "latest",
34
34
  "@rokkit/states": "latest",
35
+ "@rokkit/data": "latest",
36
+ "@rokkit/input": "latest",
35
37
  "d3-scale": "^4.0.2",
36
38
  "date-fns": "^4.1.0",
37
39
  "ramda": "^0.30.1"
@@ -1,11 +1,20 @@
1
1
  <script>
2
2
  import { equals } from 'ramda'
3
- import { createEmitter, noop, getKeyFromPath, getSnippet } from '@rokkit/core'
4
- import { NestedProxy } from '@rokkit/states'
3
+ import {
4
+ createEmitter,
5
+ noop,
6
+ getKeyFromPath,
7
+ getSnippet,
8
+ defaultFields,
9
+ hasChildren
10
+ } from '@rokkit/core'
11
+ import { NestedController } from '@rokkit/states'
5
12
  import { navigator } from '@rokkit/actions'
6
13
  import Summary from './Summary.svelte'
7
14
  import Item from './Item.svelte'
15
+ import ListBody from './ListBody.svelte'
8
16
 
17
+ const eventNames = ['collapse', 'change', 'expand', 'click', 'select', 'move']
9
18
  /**
10
19
  * @typedef {Object} Props
11
20
  * @property {string} [class]
@@ -27,47 +36,45 @@
27
36
  header = null,
28
37
  footer = null,
29
38
  empty = null,
30
- stub = null,
31
- extra,
32
- ...events
39
+ oncollapse,
40
+ onexpand,
41
+ onchange,
42
+ onselect,
43
+ onmove,
44
+ ...snippets
33
45
  } = $props()
34
46
 
35
47
  let emitter = $derived(
36
- createEmitter(events, ['collapse', 'change', 'expand', 'click', 'select', 'move'])
48
+ createEmitter({ oncollapse, onexpand, onchange, onselect, onmove }, eventNames)
37
49
  )
38
- let wrapper = new NestedProxy(items, value, fields, { events, multiselect, autoCloseSiblings })
50
+ function handleAction(event) {
51
+ const { name, data } = event.detail
52
+
53
+ if (has(name, emitter)) {
54
+ value = data.value
55
+ selected = data.selected
56
+ emitter[name](data)
57
+ }
58
+ }
59
+
60
+ let wrapper = new NestedController(items, value, fields, {
61
+ multiselect,
62
+ autoCloseSiblings
63
+ })
64
+ let derivedFields = $derived({ ...defaultFields, ...fields })
39
65
  </script>
40
66
 
41
- {#snippet listItems(nodes, onchange = noop)}
42
- {#each nodes as node}
43
- {@const template = getSnippet(extra, node.get('component')) ?? stub}
44
- {@const path = getKeyFromPath(node.path)}
45
- {@const props = node.get('props') || {}}
46
- <rk-list-item
47
- role="option"
48
- data-path={path}
49
- aria-selected={node.selected}
50
- aria-current={node.focused}
51
- >
52
- {#if template}
53
- {@render template(node, props, onchange)}
54
- {:else}
55
- <Item value={node.value} fields={node.fields} />
56
- {/if}
57
- </rk-list-item>
58
- {/each}
59
- {/snippet}
60
67
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
61
68
  <rk-accordion
62
69
  class={classes}
63
70
  tabindex="0"
64
- use:navigator={{ wrapper }}
71
+ use:navigator={{ wrapper, nested: true }}
65
72
  onactivate={() => (value = wrapper.value)}
66
73
  >
67
74
  {#if header}
68
75
  <rk-header>{@render header()}</rk-header>
69
76
  {/if}
70
- {#if wrapper.nodes.length === 0}
77
+ {#if items.length === 0}
71
78
  <rk-list-item role="presentation">
72
79
  {#if empty}
73
80
  {@render empty()}
@@ -76,22 +83,31 @@
76
83
  {/if}
77
84
  </rk-list-item>
78
85
  {/if}
79
- {#each wrapper.nodes as node, index}
86
+ {#each items as item, index (index)}
87
+ {@const key = `${index}`}
88
+ {@const expanded = item[derivedFields.expanded]}
80
89
  <div
81
90
  class="flex flex-col"
82
- class:is-expanded={node.expanded}
83
- class:is-selected={node.selected}
91
+ class:is-expanded={expanded}
92
+ class:is-selected={wrapper.selectedKeys.has(key)}
84
93
  data-path={index}
85
94
  >
86
95
  <Summary
87
- bind:value={wrapper.nodes[index].value}
88
- fields={node.fields}
89
- expanded={node.expanded}
90
- hasChildren={node.hasChildren()}
96
+ bind:value={items[index]}
97
+ {fields}
98
+ {expanded}
99
+ hasChildren={hasChildren(item, derivedFields)}
91
100
  />
92
- {#if node.expanded}
101
+ {#if expanded}
93
102
  <rk-list role="listbox" tabindex="-1">
94
- {@render listItems(node.children, events.change)}
103
+ <ListBody
104
+ bind:items={items[fields.children]}
105
+ bind:value
106
+ fields={fields.fields ?? fields}
107
+ {selected}
108
+ onchange={emitter.change}
109
+ {snippets}
110
+ />
95
111
  </rk-list>
96
112
  {/if}
97
113
  </div>
@@ -16,7 +16,7 @@
16
16
  </script>
17
17
 
18
18
  <rk-crumbs class={classes}>
19
- {#each items as item, index}
19
+ {#each items as item, index (index)}
20
20
  {#if index > 0}
21
21
  <span>
22
22
  {#if separator.length === 1}
@@ -60,14 +60,14 @@
60
60
  </month-year>
61
61
  <cal-body class="flex w-full cursor-pointer flex-col p-1">
62
62
  <days-of-week class="grid grid-cols-7">
63
- {#each weekdays as day, index}
63
+ {#each weekdays as day, index (index)}
64
64
  <p class:weekend={index % 6 === 0}>
65
65
  {day.slice(0, 1)}
66
66
  </p>
67
67
  {/each}
68
68
  </days-of-week>
69
69
  <days-of-month class="grid grid-cols-7 grid-rows-5">
70
- {#each days as { day, offset, date, weekend }}
70
+ {#each days as { day, offset, date, weekend } (date)}
71
71
  {@const start = offset > 0 ? offset : 'auto'}
72
72
  <!-- svelte-ignore a11y_click_events_have_key_events -->
73
73
  <day-of-month
@@ -33,7 +33,7 @@
33
33
  <p>{value.text}</p>
34
34
  </slide>
35
35
  <dot-nav role="radiogroup">
36
- {#each items as item, index}
36
+ {#each items as item, index (index)}
37
37
  <dot
38
38
  role="radio"
39
39
  aria-checked={currentIndex === index}
@@ -23,7 +23,7 @@
23
23
  <error> Invalid schema. Expected schema to include an 'elements' array. </error>
24
24
  {:else}
25
25
  <Wrapper {...wrapperProps}>
26
- {#each schema.elements as item}
26
+ {#each schema.elements as item, index (index)}
27
27
  {@const elementPath = item.key ? [...path, item.key] : path}
28
28
  {@const props = { ...item.props, path: elementPath }}
29
29
  {@const nested = Array.isArray(item.elements) && item.elements.length > 0}
package/src/Icon.svelte CHANGED
@@ -63,6 +63,7 @@
63
63
  onclick={handleClick}
64
64
  onkeydown={(e) => e.key === 'Enter' && e.currentTarget.click()}
65
65
  data-state={state}
66
+ data-tag="icon"
66
67
  tabindex={validatedTabindex}
67
68
  onmouseenter={emitter.mouseenter}
68
69
  onnmouseleave={emitter.nmouseleave}
package/src/Link.svelte CHANGED
@@ -12,8 +12,8 @@
12
12
 
13
13
  /** @type {Props} */
14
14
  let { class: classes = '', value, mapping = new FieldMapper(), href = '#', ...rest } = $props()
15
- let url = $derived(mapping.getAttribute(value, 'url') ?? href)
16
- let props = $derived({ ...mapping.getAttribute(value, 'props'), ...rest })
15
+ let url = $derived(mapping.get('url', value, href))
16
+ let props = $derived({ ...mapping.get('props', value, {}), ...rest })
17
17
  </script>
18
18
 
19
19
  <a href={url} class={classes} {...props}>
package/src/List.svelte CHANGED
@@ -1,8 +1,8 @@
1
1
  <script>
2
- import { createEmitter, noop, getKeyFromPath, getSnippet } from '@rokkit/core'
2
+ import { createEmitter } from '@rokkit/core'
3
3
  import { navigator } from '@rokkit/actions'
4
- import Item from './Item.svelte'
5
- import { ListProxy } from '@rokkit/states'
4
+ import ListBody from './ListBody.svelte'
5
+ import { ListController } from '@rokkit/states'
6
6
  import { omit, has } from 'ramda'
7
7
 
8
8
  /**
@@ -30,21 +30,26 @@
30
30
  header,
31
31
  footer,
32
32
  empty,
33
- stub,
34
- ...events
33
+ onselect,
34
+ onchange,
35
+ onmove,
36
+ ...snippets
35
37
  } = $props()
36
38
 
39
+ let selected = $state([])
40
+
37
41
  function handleAction(event) {
38
- value = wrapper.currentNode.value
39
- console.log(event.detail)
40
- if (has(event.detail.eventName, emitter)) {
41
- emitter[event.detail.eventName](event.detail.data)
42
+ const { name, data } = event.detail
43
+
44
+ if (has(name, emitter)) {
45
+ value = data.value
46
+ selected = data.selected
47
+ emitter[name](data)
42
48
  }
43
49
  }
44
50
 
45
- let emitter = createEmitter(events, ['select', 'change', 'move'])
46
- let extra = omit(['onselect', 'onchange', 'onmove'], events)
47
- let wrapper = new ListProxy(items, value, fields, { multiSelect })
51
+ let emitter = createEmitter({ onchange, onmove, onselect }, ['select', 'change', 'move'])
52
+ let wrapper = new ListController(items, value, fields, { multiSelect })
48
53
  </script>
49
54
 
50
55
  <rk-list
@@ -59,30 +64,22 @@
59
64
  <rk-header>{@render header()}</rk-header>
60
65
  {/if}
61
66
  <rk-body>
62
- {#if wrapper.nodes.length === 0}
67
+ {#if items.length === 0}
63
68
  {#if empty}
64
69
  {@render empty()}
65
70
  {:else}
66
71
  <rk-message>No items found.</rk-message>
67
72
  {/if}
68
73
  {:else}
69
- {#each wrapper.nodes as node}
70
- {@const template = getSnippet(extra, node.get('component')) ?? stub}
71
- {@const path = getKeyFromPath(node.path)}
72
- {@const props = node.get('props') || {}}
73
- <rk-list-item
74
- role="option"
75
- data-path={path}
76
- aria-selected={node.selected}
77
- aria-current={node.focused}
78
- >
79
- {#if template}
80
- {@render template(node, props, emitter.change)}
81
- {:else}
82
- <Item value={node.value} fields={node.fields} />
83
- {/if}
84
- </rk-list-item>
85
- {/each}
74
+ <ListBody
75
+ bind:items
76
+ bind:value
77
+ {fields}
78
+ selectedKeys={wrapper.selectedKeys}
79
+ focusedKey={wrapper.focusedKey}
80
+ onchange={emitter.change}
81
+ {snippets}
82
+ />
86
83
  {/if}
87
84
  </rk-body>
88
85
  {#if footer}
@@ -0,0 +1,42 @@
1
+ <script>
2
+ import { getKeyFromPath, FieldMapper, getSnippet } from '@rokkit/core'
3
+ import Item from './Item.svelte'
4
+ import { equals } from 'ramda'
5
+
6
+ let {
7
+ items = $bindable([]),
8
+ value = null,
9
+ fields,
10
+ path = [],
11
+ onchange = () => {},
12
+ selectedKeys = new SvelteSet(),
13
+ focusedKey = null,
14
+ snippets = {}
15
+ } = $props()
16
+ let fm = $derived.by(() => {
17
+ return new FieldMapper(fields)
18
+ })
19
+ </script>
20
+
21
+ {#each items as item, index (index)}
22
+ {@const template = getSnippet(snippets, fm.get('snippet', item, 'stub'))}
23
+ {@const pathKey = getKeyFromPath([...path, index])}
24
+ {@const props = fm.get('props', item) || {}}
25
+ <rk-list-item
26
+ role="option"
27
+ data-path={pathKey}
28
+ aria-selected={selectedKeys.has(pathKey)}
29
+ aria-current={focusedKey === pathKey}
30
+ >
31
+ <svelte:boundary>
32
+ {#if template}
33
+ {@render template(item, props, onchange)}
34
+ {#snippet failed()}
35
+ <Item value={item} {fields} />
36
+ {/snippet}
37
+ {:else}
38
+ <Item value={item} {fields} />
39
+ {/if}
40
+ </svelte:boundary>
41
+ </rk-list-item>
42
+ {/each}
@@ -36,7 +36,7 @@
36
36
  >
37
37
  {#if value.length > 0}
38
38
  <items class="flex flex-wrap">
39
- {#each value as item}
39
+ {#each value as item, index (index)}
40
40
  <Item value={item} {fields} {using} removable on:remove={handleRemove} class="pill" />
41
41
  {/each}
42
42
  </items>
@@ -1,33 +1,77 @@
1
1
  <script>
2
- import { defaultStateIcons, getLineTypes } from '@rokkit/core'
2
+ import { SvelteSet } from 'svelte/reactivity'
3
+ import {
4
+ defaultStateIcons,
5
+ getLineTypes,
6
+ getKeyFromPath,
7
+ getNestedFields,
8
+ hasChildren
9
+ } from '@rokkit/core'
3
10
  import Node from './Node.svelte'
4
11
  import NestedList from './NestedList.svelte'
5
12
 
6
13
  /**
7
14
  * @typedef {Object} Props
8
- * @property {Array<NodeProxy>} [items]
9
- * @property {import('./types').ConnectionType[]} [types]
10
- * @property {any} [value]
11
- * @property {import('./types').NodeStateIcons} icons
15
+ * @property {Array<NodeProxy>} [items=[]]
16
+ * @property {any} [value=null]
17
+ * @property {import('./types').FieldMapping} fields
18
+ * @property {number[]} [path=[]]
19
+ * @property {import('./types').NodeStateIcons} icons
20
+ * @property {import('./types').ConnectionType[]} [types=[]]
21
+ * @property {string} [focusedKey]
22
+ * @property {SvelteSet} [selectedKeys]
12
23
  */
13
24
 
14
25
  /** @type {Props} */
15
- let { items = $bindable([]), types = [], value = $bindable(null), icons = {} } = $props()
26
+ let {
27
+ items = $bindable([]),
28
+ value = $bindable(null),
29
+ fields,
30
+ path = [],
31
+ icons = {},
32
+ types = [],
33
+ focusedKey,
34
+ selectedKeys = new SvelteSet(),
35
+ stub,
36
+ snippets
37
+ } = $props()
16
38
 
17
39
  const stateIcons = $derived({ ...defaultStateIcons.node, ...icons })
40
+ const childFields = $derived(getNestedFields(fields))
41
+ // $inspect(childFields, items[0], expandedKeys)
18
42
  </script>
19
43
 
20
44
  <rk-nested-list role="tree">
21
- {#each items as item, index}
22
- {@const hasChildren = item.hasChildren()}
45
+ {#each items as item, index (index)}
46
+ {@const nodePath = [...path, index]}
47
+ {@const key = getKeyFromPath(nodePath)}
48
+ {@const expanded = item[fields.expanded]}
23
49
  {@const nodeType = index === items.length - 1 ? 'last' : 'child'}
24
- {@const connectors = getLineTypes(hasChildren, types, nodeType)}
50
+ {@const connectors = getLineTypes(hasChildren(item, fields), types, nodeType)}
25
51
 
26
- <Node value={items[index]} types={connectors} {stateIcons}>
27
- {#if items[index].expanded}
28
- <!-- <div role="treeitem" aria-selected={false}> -->
29
- <NestedList items={item.children} {value} icons={stateIcons} types={connectors} />
30
- <!-- </div> -->
52
+ <Node
53
+ value={item}
54
+ {fields}
55
+ {stateIcons}
56
+ types={connectors}
57
+ focused={focusedKey === key}
58
+ selected={selectedKeys.has(key)}
59
+ {expanded}
60
+ path={nodePath}
61
+ {stub}
62
+ {snippets}
63
+ >
64
+ {#if hasChildren(item, fields) && expanded}
65
+ <NestedList
66
+ items={item[fields.children]}
67
+ {value}
68
+ fields={childFields}
69
+ path={nodePath}
70
+ icons={stateIcons}
71
+ types={connectors}
72
+ {focusedKey}
73
+ {selectedKeys}
74
+ />
31
75
  {/if}
32
76
  </Node>
33
77
  {/each}
@@ -1,6 +1,8 @@
1
1
  <script>
2
2
  import { defaultFields, flattenNestedList } from '@rokkit/core'
3
- import { Item, BreadCrumbs } from '@rokkit/molecules'
3
+ import Item from './Item.svelte'
4
+ import BreadCrumbs from './BreadCrumbs.svelte'
5
+ import Icon from './Icon.svelte'
4
6
  // import { flattenNestedList } from './lib/nested'
5
7
  import { createEventDispatcher } from 'svelte'
6
8
 
@@ -47,13 +49,15 @@
47
49
 
48
50
  $: flatList = flattenNestedList(items, fields)
49
51
  $: fields = { ...defaultFields, ...(fields ?? {}) }
50
- $: using = { default: Item, ...(using ?? {}) }
51
52
  </script>
52
53
 
53
54
  <pages>
54
- <!-- svelte-ignore a11y-click-events-have-key-events -->
55
- <icon class="arrow-left cursor-pointer" on:click={() => handleNav('previous')} />
55
+ <Icon
56
+ name="arrow-left"
57
+ class="cursor-pointer"
58
+ label="Previous"
59
+ onclick={() => handleNav('previous')}
60
+ />
56
61
  <BreadCrumbs items={trail} {fields} {using} />
57
- <!-- svelte-ignore a11y-click-events-have-key-events -->
58
- <icon class="arrow-right cursor-pointer" on:click={() => handleNav('next')} />
62
+ <Icon name="arrow-right" class="cursor-pointer" onclick={() => handleNav('next')} label="Next" />
59
63
  </pages>
package/src/Node.svelte CHANGED
@@ -5,44 +5,54 @@
5
5
  import Item from './Item.svelte'
6
6
 
7
7
  /**
8
- * @typedef {Object} Props
9
- * @property {any} value
10
- * @property {any} [types]
8
+ * @typedef {Object} Props
9
+ * @property {any} value
10
+ * @property {import('./types').FieldMapping} fields
11
+ * @property {any} [types]
11
12
  * @property {import('./types').NodeStateIcons} [stateIcons]
13
+ * @property {number[]} [path=[]]
14
+ * @property {boolean} [focused=false]
15
+ * @property {boolean} [selected=false]
16
+ * @property {boolean} [expanded=false]
17
+ * @property {Function} [children]
18
+ * @property {Function} [stub=null]
19
+ * @property {Object<string, Function>} [snippets={}]
12
20
  */
13
21
 
14
22
  /** @type {Props} */
15
23
  let {
16
- value = $bindable(),
24
+ value = $bindable(null),
25
+ fields,
17
26
  types = [],
18
27
  stateIcons = defaultStateIcons.node,
19
- stub = null,
28
+ path = [],
29
+ focused = false,
30
+ selected = false,
31
+ expanded = false,
20
32
  children,
21
- ...extra
33
+ stub = null,
34
+ snippets = {}
22
35
  } = $props()
23
36
 
37
+ let stateName = $derived(expanded ? 'opened' : 'closed')
24
38
  let icons = $derived({ ...defaultStateIcons.node, ...stateIcons })
25
- let stateName = $derived(value.expanded ? 'opened' : 'closed')
26
39
  let state = $derived(
27
- value.expanded
28
- ? { icon: icons.opened, label: 'collapse' }
29
- : { icon: icons.closed, label: 'expand' }
40
+ expanded ? { icon: icons.opened, label: 'collapse' } : { icon: icons.closed, label: 'expand' }
30
41
  )
31
42
 
32
- const template = getSnippet(value.get('component'), extra) ?? stub
33
- // $inspect(value.focused, value.expanded, value.selected, value.get('text'))
43
+ const template = getSnippet(value[fields.snippet], snippets, stub)
34
44
  </script>
35
45
 
36
46
  <rk-node
37
- aria-current={value.focused}
38
- aria-selected={value.selected}
39
- aria-expanded={value.expanded}
47
+ aria-current={focused}
48
+ aria-selected={selected}
49
+ aria-expanded={expanded}
40
50
  role="treeitem"
41
- data-path={getKeyFromPath(value.path)}
42
- data-depth={value.path.length}
51
+ data-path={getKeyFromPath(path)}
52
+ data-depth={path.length}
43
53
  >
44
54
  <div class="flex flex-row items-center">
45
- {#each types as type}
55
+ {#each types as type, index (index)}
46
56
  {#if type === 'icon'}
47
57
  <Icon name={state.icon} label={state.label} state={stateName} class="w-4" size="small" />
48
58
  {:else}
@@ -50,11 +60,16 @@
50
60
  {/if}
51
61
  {/each}
52
62
  <rk-item>
53
- {#if template}
63
+ <svelte:boundary>
64
+ <!-- {#if template} -->
54
65
  {@render template(value)}
55
- {:else}
56
- <Item value={value.value} fields={value.fields} />
57
- {/if}
66
+ {#snippet failed()}
67
+ <Item {value} {fields} />
68
+ {/snippet}
69
+ <!-- {:else}
70
+ <Item {value} {fields} />
71
+ {/if} -->
72
+ </svelte:boundary>
58
73
  </rk-item>
59
74
  </div>
60
75
  {@render children?.()}
@@ -41,7 +41,7 @@
41
41
  </script>
42
42
 
43
43
  <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
44
- <nav-pages class="grid grid-cols-3 select-none {className}" tabindex="0">
44
+ <nav-pages class="grid select-none grid-cols-3 {className}" tabindex="0">
45
45
  <!-- svelte-ignore a11y-click-events-have-key-events -->
46
46
  <span
47
47
  class="flex cursor-pointer items-center"
@@ -59,7 +59,7 @@
59
59
  </span>
60
60
  <span class="flex items-center justify-center">
61
61
  <block class="flex items-center">
62
- {#each items as item, index}
62
+ {#each items as item, index (index)}
63
63
  <!-- svelte-ignore a11y-click-events-have-key-events -->
64
64
  <pg
65
65
  class:numbers
@@ -21,7 +21,7 @@
21
21
  </script>
22
22
 
23
23
  <span class="progress flex items-center gap-2" class:empty={count === 0}>
24
- {#each steps as step}
24
+ {#each steps as step, index (index)}
25
25
  <!-- svelte-ignore a11y-click-events-have-key-events -->
26
26
  <dot
27
27
  class="step flex h-3 w-3 rounded-full border-2 border-neutral-100 bg-neutral-300"
@@ -37,9 +37,10 @@
37
37
  class="flex cursor-pointer select-none flex-col {className}"
38
38
  class:disabled={readOnly}
39
39
  >
40
- {#each options as item}
41
- {@const itemValue = mapping.getValue(item)}
42
- {@const label = mapping.getText(item)}
40
+ {#each options as item, index (index)}
41
+ {@const label = mapping.get('text', item)}
42
+ {@const itemValue = mapping.get('value', item, label)}
43
+
43
44
  {@const state = equals(itemValue, value) ? 'on' : 'off'}
44
45
 
45
46
  <label class="flex {flexDirection} items-center gap-2">
@@ -104,7 +104,7 @@
104
104
  </rk-range-track>
105
105
 
106
106
  <rk-ticks style:--count={tickItems.length - 1}>
107
- {#each tickItems as { value, label }}
107
+ {#each tickItems as { value, label }, index (index)}
108
108
  <RangeTick {label} {value} on:click={handleClick} />
109
109
  {/each}
110
110
  </rk-ticks>
package/src/Rating.svelte CHANGED
@@ -76,7 +76,7 @@
76
76
  {#if name}
77
77
  <input {name} hidden type="number" bind:value min={0} {max} readOnly={disabled} />
78
78
  {/if}
79
- {#each stars as selected, index}
79
+ {#each stars as selected, index (index)}
80
80
  {@const stateIcon = selected ? stateIcons.filled : stateIcons.empty}
81
81
  {@const label = [placeholder, index + 1, 'out of', max].join(' ')}
82
82
  <Icon
@@ -63,9 +63,9 @@
63
63
  class="overflow-hidden {className}"
64
64
  bind:clientWidth={width}
65
65
  >
66
- {#each items as item, index}
66
+ {#each items as item, index (index)}
67
67
  {@const segmentClass = 'col-' + (index + 1)}
68
- {@const props = mapping.getAttribute(item, 'props')}
68
+ {@const props = mapping.get('props', item, {})}
69
69
  {@const Template = item[mapping.fields.component]}
70
70
  {#if small && equals(index, activeIndex)}
71
71
  <rk-segment
@@ -34,7 +34,7 @@
34
34
  class="relative grid h-full w-full overflow-hidden"
35
35
  bind:clientWidth={width}
36
36
  >
37
- {#each columns as column, index}
37
+ {#each columns as column, index (index)}
38
38
  {#if index === activeIndex}
39
39
  <rk-segment
40
40
  class="slide absolute h-full w-full {column}"
@@ -21,11 +21,11 @@
21
21
  </script>
22
22
 
23
23
  <div
24
- class="stepper w-full flex flex-col items-center gap-3 border rounded p-8 shadow"
24
+ class="stepper flex w-full flex-col items-center gap-3 rounded border p-8 shadow"
25
25
  style:--count={data.length}
26
26
  >
27
27
  <row>
28
- {#each data as { text, completed, active, steps }, stage}
28
+ {#each data as { text, completed, active, steps }, stage (stage)}
29
29
  <div class="flex flex-col items-center justify-center first:col-start-2">
30
30
  <Stage {text} {completed} {active} on:click={() => handleClick({ stage })} />
31
31
  </div>
@@ -42,10 +42,10 @@
42
42
  {/each}
43
43
  </row>
44
44
  <row>
45
- {#each data as { label }, stage}
45
+ {#each data as { label }, stage (stage)}
46
46
  {#if label}
47
47
  <p
48
- class="col-span-3 w-full flex justify-center font-medium leading-loose text-center text-neutral-800"
48
+ class="col-span-3 flex w-full justify-center text-center font-medium leading-loose text-neutral-800"
49
49
  class:pending={stage > currentStage}
50
50
  >
51
51
  {label}
@@ -57,10 +57,10 @@
57
57
 
58
58
  <style lang="postcss">
59
59
  .stepper row {
60
- @apply w-full grid;
60
+ @apply grid w-full;
61
61
  grid-template-columns: repeat(var(--count), 2fr 6fr 2fr);
62
62
  }
63
63
  .pending {
64
- @apply text-neutral-500 font-light;
64
+ @apply font-light text-neutral-500;
65
65
  }
66
66
  </style>
@@ -1,5 +1,4 @@
1
1
  <script>
2
- import { defaultMapping } from './constants'
3
2
  import Item from './Item.svelte'
4
3
  /**
5
4
  * @typedef {Object} Props
package/src/Switch.svelte CHANGED
@@ -1,6 +1,6 @@
1
1
  <script>
2
2
  import { equals } from 'ramda'
3
- import { noop } from '@rokkit/core'
3
+ import { noop, getSnippet, FieldMapper } from '@rokkit/core'
4
4
  import { keyboard } from '@rokkit/actions'
5
5
  import { defaultMapping } from './constants'
6
6
 
@@ -19,10 +19,12 @@
19
19
  class: classes = '',
20
20
  value = $bindable(),
21
21
  options = [false, true],
22
- mapping = defaultMapping,
22
+ fields,
23
23
  compact = false,
24
24
  disabled = false,
25
- onchange = noop
25
+ onchange = noop,
26
+ stub,
27
+ ...extra
26
28
  } = $props()
27
29
 
28
30
  let cursor = $state([])
@@ -50,6 +52,7 @@
50
52
  prev: ['ArrowLeft', 'ArrowUp']
51
53
  }
52
54
  let useComponent = $derived(!options.every((item) => [false, true].includes(item)))
55
+ let mapper = new FieldMapper(fields)
53
56
  </script>
54
57
 
55
58
  {#if !Array.isArray(options) || options.length < 2}
@@ -72,13 +75,13 @@
72
75
  onclick={handleClick}
73
76
  >
74
77
  {#each options as item, index (item)}
75
- {@const Template = useComponent ? mapping.getComponent(item) : null}
78
+ {@const Template = getSnippet(extra, mapper.get('snippet', item), stub)}
76
79
  <rk-item class="relative" role="option" aria-selected={equals(item, value)} data-path={index}>
77
80
  {#if equals(item, value)}
78
81
  <rk-indicator class="absolute bottom-0 left-0 right-0 top-0"></rk-indicator>
79
82
  {/if}
80
83
  {#if Template}
81
- <Template value={item} {mapping} />
84
+ <Template value={item} {fields} />
82
85
  {/if}
83
86
  </rk-item>
84
87
  {/each}
@@ -31,13 +31,13 @@
31
31
  path = null
32
32
  } = $props()
33
33
 
34
- const Template = $derived(mapping.getComponent(value))
34
+ const Template = $derived(mapping.get('component', value))
35
35
  </script>
36
36
 
37
37
  <td class={classes}>
38
38
  <rk-cell>
39
39
  {#if path}
40
- {#each levels.slice(0, -1) as _}
40
+ {#each levels.slice(0, -1) as level (level)}
41
41
  <Connector type="empty" />
42
42
  {/each}
43
43
  {#if isParent}
package/src/Tabs.svelte CHANGED
@@ -1,6 +1,6 @@
1
1
  <script>
2
- import { defaultFields, defaultStateIcons, noop, getSnippet } from '@rokkit/core'
3
- import { ListProxy } from '@rokkit/states'
2
+ import { defaultFields, defaultStateIcons, noop, getSnippet, FieldMapper } from '@rokkit/core'
3
+ import { ListController } from '@rokkit/states'
4
4
  import { navigator } from '@rokkit/actions'
5
5
  import Icon from './Icon.svelte'
6
6
  import Item from './Item.svelte'
@@ -34,8 +34,6 @@
34
34
  ...extra
35
35
  } = $props()
36
36
 
37
- let cursor = $state([])
38
-
39
37
  function handleRemove(event) {
40
38
  if (typeof event.detail === Object) {
41
39
  event.detail[fields.isDeleted] = true
@@ -56,8 +54,9 @@
56
54
  onselect({ item: value, indices: cursor })
57
55
  }
58
56
  let stateIcons = $derived({ ...defaultStateIcons.action, ...icons })
59
- let filtered = $derived(options.filter((item) => !item[fields.isDeleted]))
60
- let wrapper = new ListProxy(options, value, fields)
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)
61
60
  </script>
62
61
 
63
62
  <rk-tabs
@@ -67,15 +66,19 @@
67
66
  class:justify-end={align === 'right'}
68
67
  tabindex="0"
69
68
  role="listbox"
70
- use:navigator={{ wrapper }}
69
+ use:navigator={{ wrapper, horizontal: true }}
71
70
  onaction={handleNav}
72
71
  onremove={handleRemove}
73
72
  onadd={handleAdd}
74
73
  >
75
- {#each wrapper.nodes as item, index}
76
- {@const Template = getSnippet(extra, item.get('component')) ?? stub}
74
+ {#each filtered as item, index (index)}
75
+ {@const Template = getSnippet(extra, mapper.get('snippet', item), stub)}
77
76
  <rk-tab>
78
- <Template value={item} {mapping} />
77
+ {#if Template}
78
+ <Template value={item} {fields} />
79
+ {:else}
80
+ <Item value={item} {fields} />
81
+ {/if}
79
82
  {#if editable}
80
83
  <Icon
81
84
  name="remove"
package/src/Toggle.svelte CHANGED
@@ -1,8 +1,7 @@
1
1
  <script>
2
- import { FieldMapper, noop } from '@rokkit/core'
2
+ // import { FieldMapper, noop } from '@rokkit/core'
3
3
  import { keyboard } from '@rokkit/actions'
4
4
  import Item from './Item.svelte'
5
- import { defaultMapping } from './constants'
6
5
 
7
6
  /**
8
7
  * @typedef {Object} Props
@@ -17,8 +16,9 @@
17
16
  class: classes = '',
18
17
  value = $bindable(null),
19
18
  options = [false, true],
20
- mapping = defaultMapping,
21
- onchange = noop
19
+ fields,
20
+ label = 'toggle',
21
+ onchange
22
22
  } = $props()
23
23
 
24
24
  const keyMappings = {
@@ -37,7 +37,7 @@
37
37
  }
38
38
 
39
39
  value = options[nextIndex]
40
- onchange(value)
40
+ onchange?.(value)
41
41
  }
42
42
  </script>
43
43
 
@@ -47,8 +47,8 @@
47
47
  onnext={() => toggle()}
48
48
  onprev={() => toggle(-1)}
49
49
  onclick={() => toggle()}
50
- aria-label={mapping.getLabel(value)}
50
+ aria-label={label}
51
51
  >
52
- <Item {value} {mapping} />
52
+ <Item {value} {fields} />
53
53
  </button>
54
54
  </rk-toggle>
package/src/Tree.svelte CHANGED
@@ -1,8 +1,8 @@
1
1
  <script>
2
- import { createEmitter } from '@rokkit/core'
2
+ import { createEmitter, defaultFields } from '@rokkit/core'
3
3
  import { navigator } from '@rokkit/actions'
4
4
  import NestedList from './NestedList.svelte'
5
- import { NestedProxy } from '@rokkit/states'
5
+ import { NestedController } from '@rokkit/states'
6
6
  import { omit, has } from 'ramda'
7
7
  /**
8
8
  * @typedef {Object} Props
@@ -13,6 +13,10 @@
13
13
  * @property {import('./types').NodeStateIcons|Object} [icons]
14
14
  * @property {boolean} [autoCloseSiblings=false]
15
15
  * @property {boolean} [multiselect=false]
16
+ * @property {Function} [header]
17
+ * @property {Function} [footer]
18
+ * @property {Function} [empty]
19
+ * @property {Function} [stub]
16
20
  */
17
21
 
18
22
  /** @type {Props & { [key: string]: any }} */
@@ -24,37 +28,52 @@
24
28
  icons = {},
25
29
  autoCloseSiblings = false,
26
30
  multiselect = false,
27
- keys = null,
28
31
  header,
29
32
  footer,
30
33
  empty,
34
+ stub,
31
35
  ...events
32
36
  } = $props()
33
37
 
34
- let emitter = createEmitter(events, ['select', 'move', 'collapse', 'expand'])
35
- let wrapper = new NestedProxy(items, value, fields, { autoCloseSiblings, multiselect })
38
+ let emitter = createEmitter(events, ['select', 'move', 'toggle'])
39
+ let wrapper = new NestedController(items, value, fields, { autoCloseSiblings, multiselect })
40
+ let snippets = omit(['onselect', 'onmove', 'ontoggle'], events)
41
+ let derivedFields = $derived({ ...defaultFields, ...fields })
36
42
 
37
43
  function handleAction(event) {
38
- const { eventName, data } = event.detail
39
- if (eventName === 'select') value = wrapper.currentNode?.value
40
-
41
- if (has([eventName], emitter)) emitter[eventName](data)
44
+ const { name, data } = event.detail
45
+ if (name === 'select') value = data.value
46
+ if (has([name], emitter)) emitter[name](data)
42
47
  }
43
48
  </script>
44
49
 
45
50
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
46
- <rk-tree tabindex="0" class={classes} use:navigator={{ wrapper }} onaction={handleAction}>
51
+ <rk-tree
52
+ tabindex="0"
53
+ class={classes}
54
+ use:navigator={{ wrapper, nested: true }}
55
+ onaction={handleAction}
56
+ >
47
57
  {#if header}
48
58
  <rk-header>{@render header()}</rk-header>
49
59
  {/if}
50
- {#if wrapper.nodes.length === 0}
60
+ {#if items.length === 0}
51
61
  {#if empty}
52
62
  {@render empty()}
53
63
  {:else}
54
64
  <div class="m-auto p-4 text-center text-gray-500">No data available</div>
55
65
  {/if}
56
66
  {/if}
57
- <NestedList items={wrapper.nodes} {value} {icons} />
67
+ <NestedList
68
+ {items}
69
+ fields={derivedFields}
70
+ {value}
71
+ {icons}
72
+ focusedKey={wrapper.currentKey}
73
+ selectedKeys={wrapper.selectedKeys}
74
+ {stub}
75
+ {snippets}
76
+ />
58
77
  {#if footer}
59
78
  <rk-footer>{@render footer()}</rk-footer>
60
79
  {/if}
@@ -120,21 +120,21 @@
120
120
  <table class:striped>
121
121
  <thead>
122
122
  <tr>
123
- {#each columns as col}
123
+ {#each columns as col (col.key)}
124
124
  <th>{col.label ?? col.key}</th>
125
125
  {/each}
126
126
  </tr>
127
127
  </thead>
128
128
  <tbody>
129
- {#each visible as item, index}
129
+ {#each visible as item, index (index)}
130
130
  <tr
131
131
  class:cursor-pointer={!item._isParent}
132
132
  aria-current={currentItem === item}
133
133
  onclick={stopPropagation((e) => handleItemClick(e, item))}
134
134
  >
135
- {#each columns as col, index}
135
+ {#each columns as col, index (index)}
136
136
  {@const value = { ...pick(['icon'], col), ...item }}
137
- {@const SvelteComponent = mapping.getComponent(item)}
137
+ {@const SvelteComponent = mapping.get('component', item)}
138
138
  <td>
139
139
  <cell>
140
140
  {#if multiselect && index === 0}
@@ -147,7 +147,7 @@
147
147
  <!-- {/if} -->
148
148
  {:else}
149
149
  {#if col.path}
150
- {#each item._levels.slice(0, -1) as _}
150
+ {#each item._levels.slice(0, -1) as _, index (index)}
151
151
  <Connector type="empty" />
152
152
  {/each}
153
153
  {#if item._isParent}
@@ -14,7 +14,7 @@
14
14
  </script>
15
15
 
16
16
  <rk-status-report class={classes}>
17
- {#each items as { text, status }}
17
+ {#each items as { text, status }, index (index)}
18
18
  <rk-message class={status}>
19
19
  <Icon name={icons[status]} />
20
20
  <p>{text}</p>
package/src/index.js CHANGED
@@ -25,7 +25,6 @@ export { default as Toggle } from './Toggle.svelte'
25
25
  export { default as Switch } from './Switch.svelte'
26
26
  export { default as List } from './List.svelte'
27
27
  export { default as Accordion } from './Accordion.svelte'
28
- export { default as NestedList } from './NestedList.svelte'
29
28
  export { default as Tree } from './Tree.svelte'
30
29
  export { default as Tabs } from './Tabs.svelte'
31
30
  export { default as Select } from './Select.svelte'
package/src/lib/tree.js CHANGED
@@ -13,7 +13,7 @@ export function addRootNode(items, root = '/', mapping = defaultMapping) {
13
13
  return [
14
14
  {
15
15
  [mapping.fields.text]: root,
16
- [mapping.fields.isOpen]: true,
16
+ [mapping.fields.expanded]: true,
17
17
  [mapping.fields.children]: items
18
18
  }
19
19
  ]
@@ -1,13 +1,12 @@
1
1
  <script>
2
2
  import { getContext } from 'svelte'
3
- import { defaultFields, FieldMapper } from '@rokkit/core'
4
3
 
5
4
  const registry = getContext('registry')
6
5
 
7
6
  let {
8
7
  class: className = '',
9
8
  options = [],
10
- fields = defaultFields,
9
+ fields,
11
10
  navigator = 'tabs',
12
11
  type = 'vertical',
13
12
  category = null,
@@ -19,7 +18,9 @@
19
18
  </script>
20
19
 
21
20
  <section class={className}>
22
- <Template {options} {fields} bind:value={category} {...restProps} />
21
+ {#if Template}
22
+ <Template {options} {fields} bind:value={category} {...restProps} />
23
+ {/if}
23
24
  <field-layout class={type}>
24
25
  {@render children?.()}
25
26
  </field-layout>