@rokkit/ui 1.0.0-next.100 → 1.0.0-next.106
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/constants.d.ts +2 -0
- package/dist/index.d.ts +30 -0
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/tree.d.ts +9 -0
- package/dist/types.d.ts +5 -0
- package/package.json +16 -40
- package/src/Accordion.svelte +78 -0
- package/src/BreadCrumbs.svelte +33 -0
- package/src/Calendar.svelte +93 -0
- package/src/CheckBox.svelte +56 -0
- package/src/Connector.svelte +39 -0
- package/src/Icon.svelte +71 -0
- package/src/Item.svelte +24 -0
- package/src/Link.svelte +21 -0
- package/src/List.svelte +67 -0
- package/src/Message.svelte +11 -0
- package/src/NestedList.svelte +68 -0
- package/src/Node.svelte +64 -0
- package/src/Overlay.svelte +21 -0
- package/src/Pill.svelte +41 -0
- package/src/ProgressBar.svelte +21 -0
- package/src/RadioGroup.svelte +51 -0
- package/src/Range.svelte +45 -0
- package/src/RangeMinMax.svelte +124 -0
- package/src/RangeSlider.svelte +79 -0
- package/src/RangeTick.svelte +28 -0
- package/src/Rating.svelte +95 -0
- package/src/ResponsiveGrid.svelte +88 -0
- package/src/Scrollable.svelte +7 -0
- package/src/Separator.svelte +1 -0
- package/src/SlidingColumns.svelte +52 -0
- package/src/Summary.svelte +26 -0
- package/src/Switch.svelte +86 -0
- package/src/TableCell.svelte +51 -0
- package/src/TableHeaderCell.svelte +54 -0
- package/src/Tabs.svelte +88 -0
- package/src/Toggle.svelte +54 -0
- package/src/ToggleThemeMode.svelte +19 -0
- package/src/Tree.svelte +53 -0
- package/src/TreeTable.svelte +170 -0
- package/src/ValidationReport.svelte +23 -0
- package/src/constants.js +4 -0
- package/src/index.js +34 -8
- package/src/lib/index.js +1 -0
- package/src/lib/tree.js +22 -0
- package/src/types.js +9 -0
- package/LICENSE +0 -21
- package/README.md +0 -3
- package/src/wrappers.js +0 -2
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { equals } from 'ramda'
|
|
3
|
+
import { defaultStateIcons, getLineTypes, getKeyFromPath } from '@rokkit/core'
|
|
4
|
+
import { defaultMapping } from './constants'
|
|
5
|
+
import Node from './Node.svelte'
|
|
6
|
+
import NestedList from './NestedList.svelte'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} Props
|
|
10
|
+
* @property {string} [class]
|
|
11
|
+
* @property {Array<any>} [items]
|
|
12
|
+
* @property {import('@rokkit/core').FieldMapping} [mapping]
|
|
13
|
+
* @property {import('./types').ConnectionType[]} [types]
|
|
14
|
+
* @property {any} [value]
|
|
15
|
+
* @property {number[]} [hierarchy]
|
|
16
|
+
* @property {import('./types').NodeStateIcons} icons
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {Props} */
|
|
20
|
+
let {
|
|
21
|
+
class: classes = '',
|
|
22
|
+
items = $bindable([]),
|
|
23
|
+
wrapper = $bindable(),
|
|
24
|
+
mapping = defaultMapping,
|
|
25
|
+
types = [],
|
|
26
|
+
value = $bindable(null),
|
|
27
|
+
hierarchy = [],
|
|
28
|
+
icons = {}
|
|
29
|
+
} = $props()
|
|
30
|
+
|
|
31
|
+
const stateIcons = $derived({ ...defaultStateIcons.node, ...icons })
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<rk-nested-list class={classes} role="tree">
|
|
35
|
+
{#each items as item, index}
|
|
36
|
+
{@const hasChildren = mapping.hasChildren(item)}
|
|
37
|
+
{@const indexPath = [...hierarchy, index]}
|
|
38
|
+
{@const nodeType = index === items.length - 1 ? 'last' : 'child'}
|
|
39
|
+
{@const connectors = getLineTypes(hasChildren, types, nodeType)}
|
|
40
|
+
{@const selected = wrapper.selected.has(getKeyFromPath(indexPath))}
|
|
41
|
+
{@const expanded = mapping.isExpanded(items[index])}
|
|
42
|
+
|
|
43
|
+
<Node
|
|
44
|
+
value={items[index]}
|
|
45
|
+
{mapping}
|
|
46
|
+
types={connectors}
|
|
47
|
+
path={indexPath}
|
|
48
|
+
{stateIcons}
|
|
49
|
+
{selected}
|
|
50
|
+
{expanded}
|
|
51
|
+
current={equals(wrapper.currentNode, item)}
|
|
52
|
+
>
|
|
53
|
+
{#if expanded}
|
|
54
|
+
<!-- <div role="treeitem" aria-selected={false}> -->
|
|
55
|
+
<NestedList
|
|
56
|
+
items={item[mapping.fields.children]}
|
|
57
|
+
{value}
|
|
58
|
+
{wrapper}
|
|
59
|
+
{mapping}
|
|
60
|
+
icons={stateIcons}
|
|
61
|
+
types={connectors}
|
|
62
|
+
hierarchy={indexPath}
|
|
63
|
+
/>
|
|
64
|
+
<!-- </div> -->
|
|
65
|
+
{/if}
|
|
66
|
+
</Node>
|
|
67
|
+
{/each}
|
|
68
|
+
</rk-nested-list>
|
package/src/Node.svelte
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { defaultStateIcons, getKeyFromPath } from '@rokkit/core'
|
|
3
|
+
import { defaultMapping } from './constants'
|
|
4
|
+
import Icon from './Icon.svelte'
|
|
5
|
+
import Connector from './Connector.svelte'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {Object} Props
|
|
9
|
+
* @property {any} value
|
|
10
|
+
* @property {import('@rokkit/core').FieldMapper} [mapping]
|
|
11
|
+
* @property {any} [types]
|
|
12
|
+
* @property {import('./types').NodeStateIcons} [stateIcons]
|
|
13
|
+
* @property {boolean} [selected]
|
|
14
|
+
* @property {boolean} [expanded]
|
|
15
|
+
* @property {number[]} [path]
|
|
16
|
+
* @property {import('svelte').Snippet} [children]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {Props} */
|
|
20
|
+
let {
|
|
21
|
+
class: classes = '',
|
|
22
|
+
value = $bindable(),
|
|
23
|
+
mapping = defaultMapping,
|
|
24
|
+
types = [],
|
|
25
|
+
stateIcons = defaultStateIcons.node,
|
|
26
|
+
selected = $bindable(false),
|
|
27
|
+
expanded = false,
|
|
28
|
+
current = false,
|
|
29
|
+
path = [],
|
|
30
|
+
children
|
|
31
|
+
} = $props()
|
|
32
|
+
|
|
33
|
+
let icons = $derived({ ...defaultStateIcons.node, ...stateIcons })
|
|
34
|
+
let stateName = $derived(expanded ? 'opened' : 'closed')
|
|
35
|
+
let state = $derived(
|
|
36
|
+
expanded ? { icon: icons.opened, label: 'collapse' } : { icon: icons.closed, label: 'expand' }
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const Template = $derived(mapping.getComponent(value))
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<rk-node
|
|
43
|
+
class={classes}
|
|
44
|
+
aria-current={current}
|
|
45
|
+
aria-selected={selected}
|
|
46
|
+
aria-expanded={state.label === 'collapse'}
|
|
47
|
+
role="treeitem"
|
|
48
|
+
data-path={getKeyFromPath(path)}
|
|
49
|
+
data-depth={path.length}
|
|
50
|
+
>
|
|
51
|
+
<div class="flex flex-row items-center">
|
|
52
|
+
{#each types as type}
|
|
53
|
+
{#if type === 'icon'}
|
|
54
|
+
<Icon name={state.icon} label={state.label} state={stateName} class="w-4" size="small" />
|
|
55
|
+
{:else}
|
|
56
|
+
<Connector {type} />
|
|
57
|
+
{/if}
|
|
58
|
+
{/each}
|
|
59
|
+
<rk-item>
|
|
60
|
+
<Template {value} {mapping} />
|
|
61
|
+
</rk-item>
|
|
62
|
+
</div>
|
|
63
|
+
{@render children?.()}
|
|
64
|
+
</rk-node>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { dismissable } from '@rokkit/actions'
|
|
3
|
+
let { children } = $props()
|
|
4
|
+
let dismissed = $state(false)
|
|
5
|
+
let ondismiss = () => {
|
|
6
|
+
dismissed = true
|
|
7
|
+
}
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<rk-overlay
|
|
11
|
+
class="absolute left-0 top-0 h-screen w-screen"
|
|
12
|
+
role="presentation"
|
|
13
|
+
class:z-1={!dismissed}
|
|
14
|
+
class:display-none={dismissed}
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
onclick={ondismiss}
|
|
17
|
+
use:dismissable
|
|
18
|
+
{ondismiss}
|
|
19
|
+
>
|
|
20
|
+
{@render children?.()}
|
|
21
|
+
</rk-overlay>
|
package/src/Pill.svelte
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { FieldMapper, noop } from '@rokkit/core'
|
|
3
|
+
import Item from './Item.svelte'
|
|
4
|
+
import Icon from './Icon.svelte'
|
|
5
|
+
import { keyboard } from '@rokkit/actions'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @type {Object} Props
|
|
9
|
+
* @property {string|Object} [value]
|
|
10
|
+
* @property {FieldMapper} [mapping]
|
|
11
|
+
* @property {boolean} [removable]
|
|
12
|
+
* @property {boolean} [disabled]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @type {Props} */
|
|
16
|
+
let {
|
|
17
|
+
value,
|
|
18
|
+
class: classes = '',
|
|
19
|
+
mapping = new FieldMapper(),
|
|
20
|
+
removable = false,
|
|
21
|
+
disabled = false,
|
|
22
|
+
onremove = noop
|
|
23
|
+
} = $props()
|
|
24
|
+
|
|
25
|
+
const keyMappings = {
|
|
26
|
+
remove: ['Delete', 'Backspace']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function handle(event) {
|
|
30
|
+
if (!disabled) onremove(value)
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
35
|
+
<rk-pill use:keyboard={keyMappings} onremove={handle} tabindex="0" class={classes}>
|
|
36
|
+
<Item {value} {mapping}></Item>
|
|
37
|
+
{#if removable}
|
|
38
|
+
<Icon name="action-close" role="button" aria-label="Remove" {disabled} onclick={handle} small
|
|
39
|
+
></Icon>
|
|
40
|
+
{/if}
|
|
41
|
+
</rk-pill>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { isNil } from 'ramda'
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Props
|
|
5
|
+
* @property {string} [class]
|
|
6
|
+
* @property {any} [value]
|
|
7
|
+
* @property {any} [max]
|
|
8
|
+
* @property {string} [height]
|
|
9
|
+
* @property {number} [width]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** @type {Props} */
|
|
13
|
+
let { class: classes = '', value = null, max = 100, height = '1.5mm' } = $props()
|
|
14
|
+
|
|
15
|
+
let indeterminate = $derived(isNil(value) || isNil(max))
|
|
16
|
+
let percentage = $derived(indeterminate ? '100%' : `${(value * 100) / max}%`)
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<rk-progress class:indeterminate class={classes} style:height>
|
|
20
|
+
<value-bar class="h-full" style:width={percentage}></value-bar>
|
|
21
|
+
</rk-progress>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { equals } from 'ramda'
|
|
3
|
+
import { defaultStateIcons } from '@rokkit/core'
|
|
4
|
+
import { defaultMapping } from './constants'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} Props
|
|
8
|
+
* @property {string} [class]
|
|
9
|
+
* @property {any} value
|
|
10
|
+
* @property {any} name
|
|
11
|
+
* @property {any} [id]
|
|
12
|
+
* @property {import('@rokkit/core).FieldMapper} [mapping]
|
|
13
|
+
* @property {any} [options]
|
|
14
|
+
* @property {boolean} [readOnly]
|
|
15
|
+
* @property {boolean} [textAfter]
|
|
16
|
+
* @property {any} [stateIcons]
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** @type {Props} */
|
|
20
|
+
let {
|
|
21
|
+
class: className = '',
|
|
22
|
+
value = $bindable(),
|
|
23
|
+
name,
|
|
24
|
+
id = null,
|
|
25
|
+
mapping = $bindable(defaultMapping),
|
|
26
|
+
options = [],
|
|
27
|
+
readOnly = false,
|
|
28
|
+
textAfter = true,
|
|
29
|
+
stateIcons = defaultStateIcons.radio
|
|
30
|
+
} = $props()
|
|
31
|
+
|
|
32
|
+
let flexDirection = $derived(textAfter ? 'flex-row' : 'flex-row-reverse')
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<rk-radio-group
|
|
36
|
+
{id}
|
|
37
|
+
class="flex cursor-pointer select-none flex-col {className}"
|
|
38
|
+
class:disabled={readOnly}
|
|
39
|
+
>
|
|
40
|
+
{#each options as item}
|
|
41
|
+
{@const itemValue = mapping.getValue(item)}
|
|
42
|
+
{@const label = mapping.getText(item)}
|
|
43
|
+
{@const state = equals(itemValue, value) ? 'on' : 'off'}
|
|
44
|
+
|
|
45
|
+
<label class="flex {flexDirection} items-center gap-2">
|
|
46
|
+
<input hidden type="radio" {name} bind:group={value} value={itemValue} {readOnly} />
|
|
47
|
+
<icon class={stateIcons[state]}></icon>
|
|
48
|
+
<p>{label}</p>
|
|
49
|
+
</label>
|
|
50
|
+
{/each}
|
|
51
|
+
</rk-radio-group>
|
package/src/Range.svelte
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import RangeMinMax from './RangeMinMax.svelte'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {Object} Props
|
|
6
|
+
* @property {any} [name]
|
|
7
|
+
* @property {string} [class]
|
|
8
|
+
* @property {number} [min]
|
|
9
|
+
* @property {number} [max]
|
|
10
|
+
* @property {number} [value]
|
|
11
|
+
* @property {number} [step]
|
|
12
|
+
* @property {number} [ticks]
|
|
13
|
+
* @property {number} [labelSkip]
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {Props} */
|
|
17
|
+
let {
|
|
18
|
+
name = null,
|
|
19
|
+
class: classes = '',
|
|
20
|
+
min = 0,
|
|
21
|
+
max = 100,
|
|
22
|
+
value = $bindable(min),
|
|
23
|
+
step = 1,
|
|
24
|
+
ticks = 10,
|
|
25
|
+
labelSkip = 0
|
|
26
|
+
} = $props()
|
|
27
|
+
|
|
28
|
+
let bounds = $state([min, value])
|
|
29
|
+
|
|
30
|
+
$effect(() => {
|
|
31
|
+
value = bounds[1]
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<RangeMinMax
|
|
36
|
+
bind:value={bounds}
|
|
37
|
+
{name}
|
|
38
|
+
{min}
|
|
39
|
+
{max}
|
|
40
|
+
{step}
|
|
41
|
+
{ticks}
|
|
42
|
+
{labelSkip}
|
|
43
|
+
single
|
|
44
|
+
class={classes}
|
|
45
|
+
/>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { generateTicks } from '@rokkit/core'
|
|
3
|
+
import { scaleLinear } from 'd3-scale'
|
|
4
|
+
import { onMount } from 'svelte'
|
|
5
|
+
import RangeTick from './RangeTick.svelte'
|
|
6
|
+
import RangeSlider from './RangeSlider.svelte'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {Object} Props
|
|
10
|
+
* @property {string} [class]
|
|
11
|
+
* @property {any} [name]
|
|
12
|
+
* @property {number} [min]
|
|
13
|
+
* @property {number} [max]
|
|
14
|
+
* @property {[number,number]} [value]
|
|
15
|
+
* @property {boolean} [single]
|
|
16
|
+
* @property {number} [step]
|
|
17
|
+
* @property {number} [ticks]
|
|
18
|
+
* @property {number} [labelSkip]
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** @type {Props} */
|
|
22
|
+
let {
|
|
23
|
+
class: classes = '',
|
|
24
|
+
name = null,
|
|
25
|
+
min = 0,
|
|
26
|
+
max = 100,
|
|
27
|
+
value = $bindable([min, min]),
|
|
28
|
+
single = false,
|
|
29
|
+
step = 1,
|
|
30
|
+
ticks = 10,
|
|
31
|
+
labelSkip = 0
|
|
32
|
+
} = $props()
|
|
33
|
+
|
|
34
|
+
let limits = $state([0, 0])
|
|
35
|
+
let lower = $state(min)
|
|
36
|
+
let upper = $state(min)
|
|
37
|
+
let scale = $state()
|
|
38
|
+
let width = $state()
|
|
39
|
+
|
|
40
|
+
let selectedPos = $derived(`${lower}px`)
|
|
41
|
+
let selectedWidth = $derived(`${upper - lower}px`)
|
|
42
|
+
|
|
43
|
+
function updateScale(width, min, max) {
|
|
44
|
+
if (width) {
|
|
45
|
+
limits = [0, width]
|
|
46
|
+
scale = scaleLinear().domain(limits).range([min, max])
|
|
47
|
+
lower = scale.invert(Math.max(value[0], min))
|
|
48
|
+
upper = scale.invert(value[1])
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function handleClick(event) {
|
|
52
|
+
const distance = [Math.abs(event.detail - value[0]), Math.abs(event.detail - value[1])]
|
|
53
|
+
const index = single ? 1 : distance[0] < distance[1] ? 0 : 1
|
|
54
|
+
|
|
55
|
+
value[index] = event.detail
|
|
56
|
+
if (index === 0) {
|
|
57
|
+
lower = scale.invert(event.detail)
|
|
58
|
+
} else {
|
|
59
|
+
upper = scale.invert(event.detail)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let tickStep = $derived(Math.max(1, Math.round((max - min) / ticks)))
|
|
64
|
+
let tickItems = $derived(generateTicks(min, max, tickStep, labelSkip + 1))
|
|
65
|
+
let steps = $derived(
|
|
66
|
+
step > 0
|
|
67
|
+
? Array.from({ length: 1 + (max - min) / step }, (_, i) => Math.min(min + i * step, max))
|
|
68
|
+
: []
|
|
69
|
+
)
|
|
70
|
+
onMount(() => updateScale(width, min, max))
|
|
71
|
+
</script>
|
|
72
|
+
|
|
73
|
+
{#if !Array.isArray(value)}
|
|
74
|
+
<error>Expected value to be an array</error>
|
|
75
|
+
{:else}
|
|
76
|
+
<rk-input-range class={classes}>
|
|
77
|
+
<input {name} type="hidden" bind:value />
|
|
78
|
+
<rk-range-track class="relative grid">
|
|
79
|
+
<rk-range-track-bar class="relative col-start-2 box-border" bind:clientWidth={width}>
|
|
80
|
+
</rk-range-track-bar>
|
|
81
|
+
<rk-selected-bar
|
|
82
|
+
class="absolute col-start-2"
|
|
83
|
+
style:left={selectedPos}
|
|
84
|
+
style:width={selectedWidth}
|
|
85
|
+
></rk-selected-bar>
|
|
86
|
+
{#if !single}
|
|
87
|
+
<RangeSlider
|
|
88
|
+
bind:cx={lower}
|
|
89
|
+
bind:value={value[0]}
|
|
90
|
+
{steps}
|
|
91
|
+
{scale}
|
|
92
|
+
min={limits[0]}
|
|
93
|
+
max={upper}
|
|
94
|
+
/>
|
|
95
|
+
{/if}
|
|
96
|
+
<RangeSlider
|
|
97
|
+
bind:cx={upper}
|
|
98
|
+
bind:value={value[1]}
|
|
99
|
+
{steps}
|
|
100
|
+
{scale}
|
|
101
|
+
min={lower}
|
|
102
|
+
max={limits[1]}
|
|
103
|
+
/>
|
|
104
|
+
</rk-range-track>
|
|
105
|
+
|
|
106
|
+
<rk-ticks style:--count={tickItems.length - 1}>
|
|
107
|
+
{#each tickItems as { value, label }}
|
|
108
|
+
<RangeTick {label} {value} on:click={handleClick} />
|
|
109
|
+
{/each}
|
|
110
|
+
</rk-ticks>
|
|
111
|
+
</rk-input-range>
|
|
112
|
+
{/if}
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
rk-range-track {
|
|
116
|
+
grid-template-columns: 0.5rem auto 0.5rem;
|
|
117
|
+
}
|
|
118
|
+
rk-ticks {
|
|
119
|
+
display: grid;
|
|
120
|
+
grid-gap: calc((100% - 1rem * (var(--count) + 1)) / var(--count));
|
|
121
|
+
grid-template-columns: repeat(var(--count), 1rem) 1rem;
|
|
122
|
+
grid-template-rows: 7px auto;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { pannable } from '@rokkit/actions'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef Props
|
|
6
|
+
* @property {number} min - The minimum value of the thumb.
|
|
7
|
+
* @property {number} max - The maximum value of the thumb.
|
|
8
|
+
* @property {number} value - The current value of the thumb.
|
|
9
|
+
* @property {number} cx - The current position of the thumb.
|
|
10
|
+
* @property {number[]} steps - An array of steps within a range.
|
|
11
|
+
* @property {import('d3-scale').ScaleLinear} scale - Scale mapping the thumb's position to its value.
|
|
12
|
+
*/
|
|
13
|
+
/** @type {Props} */
|
|
14
|
+
let { min = 0, max = 100, value = $bindable(), cx = $bindable(), steps = [], scale } = $props()
|
|
15
|
+
|
|
16
|
+
function handlePanMove(event) {
|
|
17
|
+
let x = cx + event.detail.dx
|
|
18
|
+
let limits = [scale.invert(min), scale.invert(max)]
|
|
19
|
+
|
|
20
|
+
cx = Math.min(Math.max(x, limits[0]), limits[1])
|
|
21
|
+
if (steps.length > 0) {
|
|
22
|
+
const result = scale(x)
|
|
23
|
+
let index = 0
|
|
24
|
+
let matched = false
|
|
25
|
+
while (!matched && index < steps.length - 1) {
|
|
26
|
+
if (steps[index] <= result && steps[index + 1] > result) {
|
|
27
|
+
value =
|
|
28
|
+
result - steps[index] > steps[index + 1] - result ? steps[index + 1] : steps[index]
|
|
29
|
+
matched = true
|
|
30
|
+
}
|
|
31
|
+
index++
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
value = scale(cx)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function handlePanEnd() {
|
|
38
|
+
sliding = false
|
|
39
|
+
if (steps.length > 0) {
|
|
40
|
+
cx = scale.invert(value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function handleKeyDown(event) {
|
|
44
|
+
if (steps.length === 0) {
|
|
45
|
+
const offset = (max - min) / 10
|
|
46
|
+
const step = event.key === 'ArrowLeft' ? -offset : event.key === 'ArrowRight' ? offset : 0
|
|
47
|
+
value = Math.min(Math.max(value + step, min), max)
|
|
48
|
+
} else {
|
|
49
|
+
const index = steps.findIndex((step) => step === value)
|
|
50
|
+
if (event.key === 'ArrowLeft' && index > 0) {
|
|
51
|
+
value = steps[index - 1]
|
|
52
|
+
cx = scale.invert(value)
|
|
53
|
+
} else if (event.key === 'ArrowRight' && index < steps.length - 1) {
|
|
54
|
+
value = steps[index + 1]
|
|
55
|
+
cx = scale.invert(value)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
let sliding = $state(false)
|
|
60
|
+
let position = $derived(`${cx}px`)
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<rk-thumb
|
|
64
|
+
class="absolute -top-1 box-border h-4 w-4 cursor-pointer"
|
|
65
|
+
style:left={position}
|
|
66
|
+
class:sliding
|
|
67
|
+
tabindex="0"
|
|
68
|
+
onfocus={() => (sliding = true)}
|
|
69
|
+
onblur={() => (sliding = false)}
|
|
70
|
+
use:pannable
|
|
71
|
+
onpanmove={handlePanMove}
|
|
72
|
+
onpanstart={() => (sliding = true)}
|
|
73
|
+
onpanend={handlePanEnd}
|
|
74
|
+
onkeydown={handleKeyDown}
|
|
75
|
+
role="slider"
|
|
76
|
+
aria-valuenow={value}
|
|
77
|
+
aria-valuemin={min}
|
|
78
|
+
aria-valuemax={max}
|
|
79
|
+
></rk-thumb>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { noop } from '@rokkit/core'
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} Props
|
|
5
|
+
* @property {string} [label]
|
|
6
|
+
* @property {number} value
|
|
7
|
+
* @property {boolean} [selected]
|
|
8
|
+
* @property {Function} [onclick]
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** @type {Props} */
|
|
12
|
+
let { label = null, value, selected = false, onclick = noop } = $props()
|
|
13
|
+
let text = $derived(label ?? value.toFixed(0))
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
17
|
+
<rk-tick
|
|
18
|
+
class="grid cursor-pointer select-none grid-cols-2"
|
|
19
|
+
{onclick}
|
|
20
|
+
role="option"
|
|
21
|
+
aria-selected={selected}
|
|
22
|
+
tabindex="0"
|
|
23
|
+
>
|
|
24
|
+
<rk-tick-bar class="h-5px col-start-2 border-l"></rk-tick-bar>
|
|
25
|
+
<p class="col-span-2 flex justify-center">
|
|
26
|
+
{text}
|
|
27
|
+
</p>
|
|
28
|
+
</rk-tick>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { createEmitter, defaultStateIcons } from '@rokkit/core'
|
|
3
|
+
import Icon from './Icon.svelte'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} Props
|
|
7
|
+
* @property {any} [id]
|
|
8
|
+
* @property {any} [name]
|
|
9
|
+
* @property {number} [value]
|
|
10
|
+
* @property {number} [max]
|
|
11
|
+
* @property {boolean} [disabled]
|
|
12
|
+
* @property {any} [stateIcons]
|
|
13
|
+
* @property {string} [placeholder]
|
|
14
|
+
* @property {number} [tabindex]
|
|
15
|
+
* @event {CustomEvent} [onchange]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** @type {Props} */
|
|
19
|
+
let {
|
|
20
|
+
id = null,
|
|
21
|
+
name = null,
|
|
22
|
+
value = $bindable(0),
|
|
23
|
+
max = 5,
|
|
24
|
+
disabled = false,
|
|
25
|
+
stateIcons = defaultStateIcons.rating,
|
|
26
|
+
placeholder = 'Rating',
|
|
27
|
+
tabindex = 0,
|
|
28
|
+
...events
|
|
29
|
+
} = $props()
|
|
30
|
+
|
|
31
|
+
let emitter = $derived(createEmitter(events, ['change']))
|
|
32
|
+
function handleClick(index) {
|
|
33
|
+
if (!disabled) {
|
|
34
|
+
value = value === 1 && index === 0 ? index : index + 1
|
|
35
|
+
emitter.change({ value })
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function handleEnter(index) {
|
|
39
|
+
if (!disabled) {
|
|
40
|
+
hoverIndex = index
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function handleLeave() {
|
|
44
|
+
if (!disabled) {
|
|
45
|
+
hoverIndex = -1
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function handleKeyDown(event) {
|
|
49
|
+
if (event.key === 'ArrowLeft' || event.key === 'ArrowDown') {
|
|
50
|
+
event.preventDefault()
|
|
51
|
+
value = Math.max(value - 1, 0)
|
|
52
|
+
} else if (event.key === 'ArrowRight' || event.key === 'ArrowUp') {
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
value = Math.min(value + 1, max)
|
|
55
|
+
} else {
|
|
56
|
+
var number = parseInt(event.code.replace('Digit', ''), 10)
|
|
57
|
+
if (number >= 0 && number <= 9 && number <= max) {
|
|
58
|
+
event.preventDefault()
|
|
59
|
+
value = number
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let hoverIndex = $state(-1)
|
|
65
|
+
let stars = $derived([...Array(max).keys()].map((i) => i < value))
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<rk-rating
|
|
69
|
+
{id}
|
|
70
|
+
class="flex cursor-pointer select-none"
|
|
71
|
+
class:disabled
|
|
72
|
+
{tabindex}
|
|
73
|
+
role="radiogroup"
|
|
74
|
+
onkeydown={handleKeyDown}
|
|
75
|
+
>
|
|
76
|
+
{#if name}
|
|
77
|
+
<input {name} hidden type="number" bind:value min={0} {max} readOnly={disabled} />
|
|
78
|
+
{/if}
|
|
79
|
+
{#each stars as selected, index}
|
|
80
|
+
{@const stateIcon = selected ? stateIcons.filled : stateIcons.empty}
|
|
81
|
+
{@const label = [placeholder, index + 1, 'out of', max].join(' ')}
|
|
82
|
+
<Icon
|
|
83
|
+
name={stateIcon}
|
|
84
|
+
{label}
|
|
85
|
+
role="option"
|
|
86
|
+
{disabled}
|
|
87
|
+
checked={index < value}
|
|
88
|
+
class={index <= hoverIndex ? 'hovering' : ''}
|
|
89
|
+
onmouseenter={() => handleEnter(index)}
|
|
90
|
+
onmouseleave={handleLeave}
|
|
91
|
+
onclick={() => handleClick(index)}
|
|
92
|
+
tabindex="-1"
|
|
93
|
+
/>
|
|
94
|
+
{/each}
|
|
95
|
+
</rk-rating>
|