@rokkit/ui 1.0.0-next.121 → 1.0.0-next.123

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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +1 -5
  3. package/dist/tree/List.spec.svelte.d.ts +1 -0
  4. package/dist/tree/Node.spec.svelte.d.ts +1 -0
  5. package/dist/tree/Root.spec.svelte.d.ts +1 -0
  6. package/package.json +4 -4
  7. package/src/BreadCrumbs.svelte +14 -19
  8. package/src/Button.svelte +3 -7
  9. package/src/Card.svelte +6 -2
  10. package/src/GraphPaper.svelte +43 -0
  11. package/src/Icon.svelte +21 -16
  12. package/src/Item.svelte +6 -6
  13. package/src/List.svelte +8 -7
  14. package/src/ListBody.svelte +3 -2
  15. package/src/PickOne.svelte +60 -0
  16. package/src/Pill.svelte +2 -2
  17. package/src/ProgressDots.svelte +1 -1
  18. package/src/Select.svelte +0 -3
  19. package/src/Stepper.svelte +2 -2
  20. package/src/Summary.svelte +1 -1
  21. package/src/Switch.svelte +45 -32
  22. package/src/Tabs.svelte +155 -75
  23. package/src/Toggle.svelte +2 -1
  24. package/src/ToggleThemeMode.svelte +7 -3
  25. package/src/index.js +1 -5
  26. package/src/tree/List.spec.svelte.js +84 -0
  27. package/src/tree/List.svelte +78 -0
  28. package/src/tree/Node.spec.svelte.js +104 -0
  29. package/src/tree/Node.svelte +80 -0
  30. package/src/tree/Root.spec.svelte.js +63 -0
  31. package/src/tree/Root.svelte +81 -0
  32. package/dist/input/types.d.ts +0 -9
  33. package/src/DataEditor.svelte +0 -31
  34. package/src/FieldLayout.svelte +0 -48
  35. package/src/Form.svelte +0 -17
  36. package/src/ListEditor.svelte +0 -44
  37. package/src/NestedEditor.svelte +0 -88
  38. package/src/input/Input.svelte +0 -17
  39. package/src/input/InputField.svelte +0 -69
  40. package/src/input/InputSelect.svelte +0 -23
  41. package/src/input/InputSwitch.svelte +0 -19
  42. package/src/input/types.js +0 -29
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { cleanup, render, fireEvent } from '@testing-library/svelte'
3
+ import Root from './Root.svelte'
4
+
5
+ describe('Tree Root Component', () => {
6
+ const items = [
7
+ { id: 1, label: 'Node 1', children: [{ id: 2, label: 'Node 1.1' }] },
8
+ { id: 3, label: 'Node 2' }
9
+ ]
10
+
11
+ beforeEach(() => cleanup())
12
+
13
+ it('renders tree root with items', () => {
14
+ const { container } = render(Root, {
15
+ props: { items, fields: { text: 'label', children: 'children' } }
16
+ })
17
+ const rootEl = container.querySelector('[data-tree-root]')
18
+ expect(rootEl).not.toBeNull()
19
+ // const contentEls = container.querySelectorAll('[data-tree-content]')
20
+ // expect(Array.from(contentEls).some((el) => el.textContent.includes('Node 1'))).toBe(true)
21
+ // expect(Array.from(contentEls).some((el) => el.textContent.includes('Node 2'))).toBe(true)
22
+ expect(rootEl).toMatchSnapshot()
23
+ })
24
+
25
+ // it('renders header and footer if provided', () => {
26
+ // const header = () => 'Header'
27
+ // const footer = () => 'Footer'
28
+ // const { container } = render(Root, {
29
+ // props: { items, fields: { label: 'label', children: 'children' }, header, footer }
30
+ // })
31
+ // expect(container.textContent).toContain('Header')
32
+ // expect(container.textContent).toContain('Footer')
33
+ // expect(container).toMatchSnapshot()
34
+ // })
35
+
36
+ it('renders empty state when items are empty', () => {
37
+ const { container } = render(Root, {
38
+ props: { items: [], fields: { label: 'label', children: 'children' } }
39
+ })
40
+ expect(container.textContent).toContain('No data available')
41
+ expect(container).toMatchSnapshot()
42
+ })
43
+
44
+ // it('calls event handlers on select', async () => {
45
+ // let selectedValue = null
46
+ // const handleSelect = (e) => {
47
+ // selectedValue = e.detail.value
48
+ // }
49
+ // const { container } = render(Root, {
50
+ // props: {
51
+ // items,
52
+ // fields: { label: 'label', children: 'children' },
53
+ // onselect: handleSelect
54
+ // }
55
+ // })
56
+ // const contentEls = container.querySelectorAll('[data-tree-content]')
57
+ // const node = Array.from(contentEls).find((el) => el.textContent.includes('Node 1'))
58
+ // expect(node).not.toBeNull()
59
+ // await fireEvent.click(node)
60
+ // expect(selectedValue).toBe(items[0])
61
+ // expect(container).toMatchSnapshot()
62
+ // })
63
+ })
@@ -0,0 +1,81 @@
1
+ <script>
2
+ import { createEmitter, defaultFields } from '@rokkit/core'
3
+ import { navigator } from '@rokkit/actions'
4
+ import List from './List.svelte'
5
+ import { NestedController } from '@rokkit/states'
6
+ import { omit, has } from 'ramda'
7
+
8
+ /**
9
+ * @typedef {Object} Props
10
+ * @property {string} [class]
11
+ * @property {Array<any>} [items]
12
+ * @property {any} [value]
13
+ * @property {import('@rokkit/core').FieldMapping} [fields]
14
+ * @property {Object} [icons]
15
+ * @property {boolean} [autoCloseSiblings=false]
16
+ * @property {boolean} [multiselect=false]
17
+ * @property {Function} [header]
18
+ * @property {Function} [footer]
19
+ * @property {Function} [empty]
20
+ * @property {Function} [stub]
21
+ */
22
+
23
+ /** @type {Props & { [key: string]: any }} */
24
+ let {
25
+ class: classes = 'h-full overflow-scroll flex flex-col',
26
+ items = $bindable([]),
27
+ value = $bindable(null),
28
+ fields,
29
+ icons = {},
30
+ autoCloseSiblings = false,
31
+ multiselect = false,
32
+ header,
33
+ footer,
34
+ empty,
35
+ stub,
36
+ ...events
37
+ } = $props()
38
+
39
+ let emitter = createEmitter(events, ['select', 'move', 'toggle'])
40
+ let wrapper = new NestedController(items, value, fields, { autoCloseSiblings, multiselect })
41
+ let snippets = omit(['onselect', 'onmove', 'ontoggle'], events)
42
+ let derivedFields = $derived({ ...defaultFields, ...fields })
43
+
44
+ function handleAction(event) {
45
+ const { name, data } = event.detail
46
+ if (name === 'select') value = data.value
47
+ if (has([name], emitter)) emitter[name](data)
48
+ }
49
+ </script>
50
+
51
+ <div
52
+ data-tree-root
53
+ tabindex="0"
54
+ class={classes}
55
+ use:navigator={{ wrapper, nested: true }}
56
+ onaction={handleAction}
57
+ >
58
+ {#if header}
59
+ <div data-tree-header>{@render header()}</div>
60
+ {/if}
61
+ {#if items.length === 0}
62
+ {#if empty}
63
+ {@render empty()}
64
+ {:else}
65
+ <div class="m-auto p-4 text-center text-gray-500" data-tree-empty>No data available</div>
66
+ {/if}
67
+ {/if}
68
+ <List
69
+ items={items}
70
+ fields={derivedFields}
71
+ value={value}
72
+ icons={icons}
73
+ focusedKey={wrapper.currentKey}
74
+ selectedKeys={wrapper.selectedKeys}
75
+ stub={stub}
76
+ snippets={snippets}
77
+ />
78
+ {#if footer}
79
+ <div data-tree-footer>{@render footer()}</div>
80
+ {/if}
81
+ </div>
@@ -1,9 +0,0 @@
1
- export namespace types {
2
- export let string: any;
3
- export let integer: any;
4
- export { CheckBox as boolean };
5
- export { InputSelect as enum };
6
- export let phone: any;
7
- }
8
- import CheckBox from '../CheckBox.svelte';
9
- import InputSelect from './InputSelect.svelte';
@@ -1,31 +0,0 @@
1
- <script>
2
- import { setContext } from 'svelte'
3
- import { writable } from 'svelte/store'
4
- import { types } from './input/types'
5
- import Wrapper from './wrappers/Wrapper.svelte'
6
- import Item from './Item.svelte'
7
- import Tabs from './Tabs.svelte'
8
- import FieldLayout from './FieldLayout.svelte'
9
- import { noop } from '@rokkit/core'
10
-
11
- const registry = $state({})
12
- setContext('registry', registry)
13
-
14
- import { deriveSchemaFromValue, deriveLayoutFromValue, getSchemaWithLayout } from './lib'
15
-
16
- let { value, schema = null, layout = null, using = {}, onchange = noop } = $props()
17
-
18
- registry.editors = { ...types, ...using?.editors }
19
- registry.components = { default: Item, ...using?.components }
20
- registry.wrappers = { default: Wrapper, ...using?.wrappers }
21
- registry.navigators = { default: Tabs, ...using?.navigators }
22
-
23
- let schemaWithLayout = $derived.by(() => {
24
- return getSchemaWithLayout(
25
- schema ?? deriveSchemaFromValue(value),
26
- layout ?? deriveLayoutFromValue(value)
27
- )
28
- })
29
- </script>
30
-
31
- <FieldLayout schema={schemaWithLayout} bind:value {onchange} />
@@ -1,48 +0,0 @@
1
- <script>
2
- import { getContext } from 'svelte'
3
- import { omit } from 'ramda'
4
- import InputField from './input/InputField.svelte'
5
- import FieldLayout from './FieldLayout.svelte'
6
-
7
- // const dispatch = createEventDispatcher()
8
- const registry = getContext('registry')
9
-
10
- export let value = {}
11
- export let schema = {}
12
- export let path = []
13
-
14
- function handle() {
15
- dispatch('change', value)
16
- }
17
-
18
- let Wrapper = registry.wrappers[schema.wrapper] ?? registry.wrappers.default
19
- let wrapperProps = omit(['wrapper', 'elements', 'key'], schema)
20
- </script>
21
-
22
- {#if !Array.isArray(schema.elements)}
23
- <error> Invalid schema. Expected schema to include an 'elements' array. </error>
24
- {:else}
25
- <Wrapper {...wrapperProps}>
26
- {#each schema.elements as item, index (index)}
27
- {@const elementPath = item.key ? [...path, item.key] : path}
28
- {@const props = { ...item.props, path: elementPath }}
29
- {@const nested = Array.isArray(item.elements) && item.elements.length > 0}
30
- {@const Component = item.component
31
- ? (registry.components[item.component] ?? registry.components.default)
32
- : null}
33
-
34
- {#if nested}
35
- {#if item.key}
36
- <FieldLayout {...props} schema={item} bind:value={value[item.key]} on:change={handle} />
37
- {:else}
38
- <FieldLayout {...props} schema={item} bind:value on:change={handle} />
39
- {/if}
40
- {:else if Component}
41
- <Component {...item.props} value={item.key ? value[item.key] : null} />
42
- {:else}
43
- {@const name = elementPath.join('.')}
44
- <InputField {name} bind:value={value[item.key]} {...item.props} on:change={handle} />
45
- {/if}
46
- {/each}
47
- </Wrapper>
48
- {/if}
package/src/Form.svelte DELETED
@@ -1,17 +0,0 @@
1
- <script>
2
- import DataEditor from './DataEditor.svelte'
3
-
4
- export let value
5
- export let schema = null
6
- export let layout = null
7
- export let using = {}
8
- </script>
9
-
10
- <form on:submit>
11
- <DataEditor bind:value {schema} {layout} {using} />
12
- <span>
13
- <slot>
14
- <button type="submit">Submit</button>
15
- </slot>
16
- </span>
17
- </form>
@@ -1,44 +0,0 @@
1
- <script>
2
- import List from './List.svelte'
3
- import FieldLayout from './FieldLayout.svelte'
4
- import { defaultFields } from '@rokkit/core'
5
- import { getContext } from 'svelte'
6
-
7
- // const dispatch = createEventDispatcher()
8
- const registry = getContext('registry')
9
-
10
- let {
11
- class: className,
12
- value,
13
- fields = defaultFields,
14
- schema,
15
- path = [],
16
- below = false,
17
- children
18
- } = $props()
19
-
20
- let index = 0
21
- let item = $state(value[index])
22
- let location = $state([...path, index])
23
-
24
- function handleSelect(event) {
25
- index = event.detail.indices[0]
26
- item = event.detail.item
27
- location = [...path, ...event.detail.indices]
28
- dispatch('select', { item: value, indices: path })
29
- }
30
- </script>
31
-
32
- <list-editor class="flex {className}">
33
- <List
34
- bind:items={value}
35
- bind:value={item}
36
- {fields}
37
- using={registry.components}
38
- on:select={handleSelect}
39
- />
40
- <item-editor class="flex" class:below>
41
- {@render children?.()}
42
- <FieldLayout bind:value={item} {schema} path={location} />
43
- </item-editor>
44
- </list-editor>
@@ -1,88 +0,0 @@
1
- <script>
2
- import { pick } from 'ramda'
3
- import { createEventDispatcher } from 'svelte'
4
- import Tree from './Tree.svelte'
5
- import DataEditor from './DataEditor.svelte'
6
- import TreeTable from './TreeTable.svelte'
7
- import Tabs from './Tabs.svelte'
8
- import { generateTreeTable, deriveNestedSchema } from './lib'
9
-
10
- const dispatch = createEventDispatcher()
11
-
12
- export let value
13
- export let schema = deriveNestedSchema(value)
14
- export let using = {}
15
- export let fields = {
16
- text: 'key',
17
- icon: 'type',
18
- iconPrefix: 'type'
19
- }
20
- let node = {
21
- schema: null,
22
- layout: null
23
- }
24
- let nodeValue = value
25
- let nodeType = null
26
- let nodeItem = null
27
- let columns = [
28
- { key: 'scope', path: true, label: 'path', fields: { text: 'key' } },
29
- { key: 'value', label: 'value', fields: { text: 'value', icon: 'type', iconPrefix: 'type' } }
30
- ]
31
-
32
- function handleChange() {
33
- dispatch('change', value)
34
- }
35
- function handleMove(event) {
36
- dispatch('change', value)
37
- const scope = event.detail.scope.split('/').slice(1)
38
-
39
- node.schema = pick(['type', 'properties', 'items'], event.detail)
40
- node.layout = event.detail.layout
41
-
42
- nodeValue = value
43
- nodeType = event.detail.type
44
-
45
- for (let i = 0; i < scope.length; i++) {
46
- nodeValue = nodeValue[scope[i]]
47
- }
48
- }
49
-
50
- $: tableData = node?.layout ? [] : generateTreeTable(nodeValue ?? value, 'scope', true)
51
- </script>
52
-
53
- <container class="flex h-full w-full flex-row">
54
- <aside class="border-r-neutral-subtle flex h-full w-80 border-r">
55
- <Tree items={schema} {fields} class="h-full w-full" on:move={handleMove} />
56
- </aside>
57
- <content class="flex h-full w-full flex-col gap-4 overflow-hidden p-8">
58
- <slot />
59
- <section class="flex w-full flex-grow flex-col overflow-auto">
60
- {#if !nodeValue}
61
- <p>Select a node to edit</p>
62
- <TreeTable data={tableData} {columns} class="" />
63
- {:else if node.layout}
64
- {#if nodeType === 'array'}
65
- <p>Arrays are not supported yet.</p>
66
- <Tabs options={nodeValue} bind:value={nodeItem} />
67
- {#if nodeItem}
68
- <DataEditor
69
- bind:value={nodeItem}
70
- layout={node.layout}
71
- schema={node.schema.items}
72
- {using}
73
- />
74
- <pre>{JSON.stringify(nodeItem, null, 2)}</pre>
75
- {/if}
76
- {:else}
77
- <DataEditor bind:value={nodeValue} {...node} {using} on:change={handleChange} />
78
- {/if}
79
- {:else}
80
- <p>
81
- No atomic attributes at this level. Select a child node to edit. Current value is below.
82
- </p>
83
- <TreeTable data={tableData} {columns} />
84
- {/if}
85
- </section>
86
- <slot name="footer" />
87
- </content>
88
- </container>
@@ -1,17 +0,0 @@
1
- <script>
2
- import { getContext } from 'svelte'
3
- import { types } from './types'
4
-
5
- const registry = getContext('registry')
6
-
7
- let { value, type = 'text', using = {}, ...restProps } = $props()
8
-
9
- using = { ...types, ...using, ...registry?.editors }
10
- let Template = using[type]
11
- </script>
12
-
13
- {#if type in using}
14
- <Template bind:value {...restProps} on:change on:focus on:blur />
15
- {:else}
16
- <error>Type "{type}" is not supported by Input</error>
17
- {/if}
@@ -1,69 +0,0 @@
1
- <script>
2
- import { getContext } from 'svelte'
3
- import { pick, omit } from 'ramda'
4
- import Icon from '../Icon.svelte'
5
- import Input from './Input.svelte'
6
- import { types } from './types'
7
-
8
- const registry = getContext('registry')
9
-
10
- let {
11
- class: className,
12
- name,
13
- value,
14
- type,
15
- required,
16
- status,
17
- disabled,
18
- message,
19
- using,
20
- nolabel,
21
- icon,
22
- label,
23
- description,
24
- ...restProps
25
- } = $props()
26
-
27
- using = { ...types, ...registry, ...using }
28
- let pass = status === 'pass'
29
- let fail = status === 'fail'
30
- let warn = status === 'warn'
31
- let rootProps = pick(['id'], restProps)
32
- let properties = {
33
- required,
34
- readOnly: disabled,
35
- ...omit(['id'], restProps),
36
- name
37
- }
38
- </script>
39
-
40
- <input-field
41
- {...rootProps}
42
- class="flex flex-col input-{type} {className} "
43
- class:disabled
44
- class:pass
45
- class:fail
46
- class:warn
47
- class:empty={!value}
48
- >
49
- {#if label && !nolabel && !['switch', 'checkbox'].includes(type)}
50
- <label for={name} class:required>
51
- {label}
52
- </label>
53
- {/if}
54
- <field class="flex w-full flex-row items-center" aria-label={description ?? label ?? name}>
55
- {#if icon}
56
- <Icon name={icon} />
57
- {/if}
58
- {#if type === 'switch'}
59
- <label for={name} class:required>{label}</label>
60
- {/if}
61
- <Input id={name} bind:value {type} {...properties} {using} on:change />
62
- {#if type === 'checkbox'}
63
- <label for={name} class:required>{label}</label>
64
- {/if}
65
- </field>
66
- {#if message}
67
- <message class={status}>{message}</message>
68
- {/if}
69
- </input-field>
@@ -1,23 +0,0 @@
1
- <script>
2
- import { getValue, defaultFields } from '@rokkit/core'
3
- import Select from '../Select.svelte'
4
-
5
- let { name, value, options = [], fields, onchange, ...restProps } = $props()
6
-
7
- let selected = $state()
8
- let configFields = $derived({ ...defaultFields, ...fields })
9
-
10
- function handle(data) {
11
- value = getValue(data.value, configFields)
12
- onchange?.(data)
13
- }
14
-
15
- $effect(() => {
16
- if (value !== getValue(selected, configFields)) {
17
- selected = options.find((option) => getValue(option, configFields) === value)
18
- }
19
- })
20
- </script>
21
-
22
- <input {name} type="hidden" bind:value />
23
- <Select name="" value={selected} {options} {fields} {...restProps} onchange={handle} />
@@ -1,19 +0,0 @@
1
- <script>
2
- import { getValue, defaultFields } from '@rokkit/core'
3
- import Switch from '../Switch.svelte'
4
-
5
- let { name, value, options, fields, ...restProps } = $props()
6
- let selected = $state(null)
7
- let configFields = $derived({ ...defaultFields, ...fields })
8
- function handle(data) {
9
- value = getValue(data.value, configFields)
10
- dispatch('change', data.value)
11
- }
12
-
13
- $effect(() => {
14
- selected = options.find((option) => getValue(option, configFields) === value)
15
- })
16
- </script>
17
-
18
- <input {name} type="hidden" bind:value />
19
- <Switch bind:value={selected} {options} {fields} {...restProps} onchange={handle} />
@@ -1,29 +0,0 @@
1
- import { toHyphenCase } from '@rokkit/core'
2
- // skipcq: JS-C1003 - Importing all components from atoms
3
- import * as NativeInput from '@rokkit/input'
4
- // skipcq: JS-C1003 - Importing all components from molecules
5
- import CheckBox from '../CheckBox.svelte'
6
-
7
- import InputSelect from './InputSelect.svelte'
8
- import InputSwitch from './InputSwitch.svelte'
9
-
10
- function extractComponentMap(components, prefix = /^Input/) {
11
- return Object.entries(components).reduce(
12
- (acc, [name, component]) => ({
13
- ...acc,
14
- [toHyphenCase(name.replace(prefix, ''))]: component
15
- }),
16
- {}
17
- )
18
- }
19
- const native = extractComponentMap(NativeInput)
20
-
21
- export const types = {
22
- string: NativeInput.InputText,
23
- integer: NativeInput.InputNumber,
24
- boolean: CheckBox,
25
- enum: InputSelect,
26
- phone: NativeInput.InputTel,
27
- ...native,
28
- ...extractComponentMap({ InputSelect, InputSwitch })
29
- }