webcoreui 1.1.0 → 1.2.0

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 (45) hide show
  1. package/README.md +9 -1
  2. package/astro.d.ts +5 -0
  3. package/astro.js +2 -0
  4. package/components/BottomNavigation/BottomNavigation.astro +1 -3
  5. package/components/Breadcrumb/Breadcrumb.astro +1 -3
  6. package/components/Carousel/Carousel.astro +79 -9
  7. package/components/Carousel/Carousel.svelte +40 -9
  8. package/components/Carousel/Carousel.tsx +46 -11
  9. package/components/Carousel/carousel.ts +3 -1
  10. package/components/Copy/Copy.astro +3 -5
  11. package/components/Copy/Copy.svelte +1 -1
  12. package/components/Copy/Copy.tsx +1 -1
  13. package/components/Input/input.ts +62 -62
  14. package/components/Modal/Modal.astro +75 -75
  15. package/components/Modal/modal.ts +25 -25
  16. package/components/OTPInput/OTPInput.astro +194 -96
  17. package/components/OTPInput/OTPInput.svelte +141 -26
  18. package/components/OTPInput/OTPInput.tsx +140 -36
  19. package/components/OTPInput/otpinput.module.scss +59 -85
  20. package/components/Pagination/Pagination.astro +3 -3
  21. package/components/Pagination/Pagination.svelte +4 -4
  22. package/components/Pagination/pagination.module.scss +3 -3
  23. package/components/RangeSlider/RangeSlider.astro +270 -0
  24. package/components/RangeSlider/RangeSlider.svelte +188 -0
  25. package/components/RangeSlider/RangeSlider.tsx +205 -0
  26. package/components/RangeSlider/rangeslider.module.scss +143 -0
  27. package/components/RangeSlider/rangeslider.ts +37 -0
  28. package/components/Sidebar/Sidebar.astro +1 -3
  29. package/components/Stepper/Stepper.astro +1 -3
  30. package/index.d.ts +23 -4
  31. package/index.js +2 -0
  32. package/package.json +109 -103
  33. package/react.d.ts +5 -0
  34. package/react.js +2 -0
  35. package/scss/global/breakpoints.scss +15 -0
  36. package/scss/setup.scss +7 -1
  37. package/svelte.d.ts +5 -0
  38. package/svelte.js +2 -0
  39. package/utils/DOMUtils.ts +27 -2
  40. package/utils/bodyFreeze.ts +1 -1
  41. package/utils/context.ts +2 -2
  42. package/utils/getBreakpoint.ts +17 -0
  43. package/utils/isOneOf.ts +5 -0
  44. package/utils/modal.ts +54 -55
  45. package/utils/toast.ts +1 -1
@@ -8,7 +8,7 @@
8
8
  import styles from './otpinput.module.scss'
9
9
 
10
10
  let {
11
- name,
11
+ name = 'otp',
12
12
  disabled,
13
13
  length = 6,
14
14
  groupLength = 0,
@@ -25,44 +25,159 @@
25
25
  className
26
26
  ])
27
27
 
28
- const inputPlaceholders = Array.from({ length }, (_, i) => i + 1)
28
+ const inputs = Array.from({ length }, (_, i) => i + 1)
29
29
  .reduce<(number | string)[]>((acc, num, i) =>
30
30
  groupLength > 0 && i % groupLength === 0 && i !== 0
31
31
  ? [...acc, separator, num]
32
32
  : [...acc, num]
33
33
  , [])
34
34
 
35
- const getAdjustedIndex = (index: number) => inputPlaceholders
36
- .slice(0, index)
37
- .filter(placeholder => typeof placeholder !== 'string')
38
- .length
35
+ const focus = (direction: 'next' | 'prev', wrapper: HTMLElement | null, clear?: boolean) => {
36
+ const index = Number(wrapper?.dataset.active)
37
+ const nextIndex = direction === 'next' ? index + 1 : index - 1
38
+
39
+ const input = wrapper?.querySelector(`[data-index="${nextIndex}"]`)
40
+
41
+ if (input instanceof HTMLInputElement) {
42
+ input.focus()
43
+
44
+ if (clear) {
45
+ input.value = ''
46
+ }
47
+ }
48
+ }
49
+
50
+ const handleKeyDown = (event: KeyboardEvent) => {
51
+ const target = event.target as HTMLInputElement
52
+
53
+ if (event.key === 'Backspace' || event.key === 'Delete') {
54
+ if (!target.value) {
55
+ focus('prev', target.parentElement, true)
56
+ }
57
+ }
58
+
59
+ if (event.key === 'ArrowLeft') {
60
+ focus('prev', target.parentElement)
61
+ }
62
+
63
+ if (event.key === 'ArrowRight') {
64
+ focus('next', target.parentElement)
65
+ }
66
+ }
67
+
68
+ const handleInput = (event: Event) => {
69
+ const target = event.target
70
+
71
+ if (!(target instanceof HTMLInputElement)) {
72
+ return
73
+ }
74
+
75
+ const index = Number(target.dataset.index)
76
+ const emptyIndex = Array.from(target.parentElement?.querySelectorAll('input') || [])
77
+ .findIndex(element => !element.value) + 1
78
+
79
+ if (emptyIndex !== 0 && emptyIndex < index) {
80
+ const emptyElement = target.parentElement?.querySelector(`[data-index="${emptyIndex}"]`)
81
+ const nextFocusElement = target.parentElement?.querySelector(`[data-index="${emptyIndex + 1}"]`)
82
+
83
+ if (emptyElement instanceof HTMLInputElement) {
84
+ emptyElement.value = target.value
85
+ }
86
+
87
+ if (nextFocusElement instanceof HTMLInputElement) {
88
+ nextFocusElement.focus()
89
+ }
90
+
91
+ target.value = ''
92
+
93
+ return
94
+ }
95
+
96
+ if (target.value) {
97
+ focus('next', target.parentElement)
98
+ }
99
+ }
100
+
101
+ const handlePaste = (event: ClipboardEvent) => {
102
+ event.preventDefault()
103
+
104
+ const target = event.target
105
+ const container = target instanceof HTMLInputElement ? target.parentElement : null
106
+
107
+ if (container) {
108
+ const paste = event.clipboardData?.getData('text') ?? ''
109
+ const nextIndex = Math.min(paste.length + 1, length)
110
+ const focusInput = container.querySelector(`[data-index="${nextIndex}"]`)
111
+
112
+ if (focusInput instanceof HTMLInputElement) {
113
+ focusInput.focus()
114
+ }
115
+
116
+ paste.split('').slice(0, length).forEach((char, i) => {
117
+ const input = container.querySelector(`[data-index="${i + 1}"]`)
118
+
119
+ if (input instanceof HTMLInputElement) {
120
+ input.value = char
121
+ }
122
+ })
123
+ }
124
+ }
125
+
126
+ const handleFocus = (event: Event) => {
127
+ const target = event.target
128
+
129
+ if (target instanceof HTMLInputElement) {
130
+ if (target.parentElement) {
131
+ target.parentElement.dataset.active = target.dataset.index
132
+ }
133
+
134
+ setTimeout(() => target.select(), 0)
135
+ }
136
+ }
137
+
138
+ const handleKeyUp = (event: Event) => {
139
+ const target = event.target
140
+ const container = target instanceof HTMLInputElement ? target.parentElement : null
141
+
142
+ if (container) {
143
+ const newValue = Array.from(container.querySelectorAll('input') || [])
144
+ .map(input => input.value)
145
+ .join('')
146
+
147
+ value = newValue
148
+ }
149
+ }
39
150
  </script>
40
151
 
41
152
  <div class={classes}>
42
153
  {#if label}
43
- <label for={name} class={styles.label}>{@html label}</label>
154
+ <label for={`${name}-0`} class={styles.label}>{@html label}</label>
44
155
  {/if}
45
156
 
46
157
  <div class={styles['input-wrapper']}>
47
- <Input
48
- name={name || 'otp'}
49
- disabled={disabled}
50
- maxlength={length}
51
- required={true}
52
- bind:value
53
- {...rest}
54
- />
55
-
56
- <div class={styles.placeholders}>
57
- {#each inputPlaceholders as placeholder, index}
58
- <div
59
- class={typeof placeholder === 'string' ? styles.separator : styles.placeholder}
60
- data-active={getAdjustedIndex(index) === value.length ? true : undefined}
61
- >
62
- {typeof placeholder === 'string' ? placeholder : value[getAdjustedIndex(index)]}
63
- </div>
64
- {/each}
65
- </div>
158
+ {#each inputs as input, index}
159
+ {#if typeof input === 'string'}
160
+ <div class={styles.separator}>{input}</div>
161
+ {:else}
162
+ <Input
163
+ id={`${name}-${index}`}
164
+ className={styles.input}
165
+ type="text"
166
+ maxlength="1"
167
+ disabled={disabled}
168
+ inputmode="numeric"
169
+ autocomplete="one-time-code"
170
+ data-index={input}
171
+ aria-label={`OTP digit ${input + 1}`}
172
+ onkeydown={handleKeyDown}
173
+ onKeyUp={handleKeyUp}
174
+ onfocus={handleFocus}
175
+ onInput={handleInput}
176
+ onpaste={handlePaste}
177
+ {...rest}
178
+ />
179
+ {/if}
180
+ {/each}
66
181
  </div>
67
182
 
68
183
  {#if subText}
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react'
1
+ import React from 'react'
2
2
  import type { OTPInputProps } from './otpinput'
3
3
 
4
4
  import Input from '../Input/Input.tsx'
@@ -8,7 +8,7 @@ import { classNames } from '../../utils/classNames'
8
8
  import styles from './otpinput.module.scss'
9
9
 
10
10
  const OTPInput = ({
11
- name,
11
+ name = 'otp',
12
12
  disabled,
13
13
  length = 6,
14
14
  groupLength = 0,
@@ -16,68 +16,172 @@ const OTPInput = ({
16
16
  label,
17
17
  subText,
18
18
  className,
19
- value,
20
19
  onChange,
21
20
  ...rest
22
21
  }: OTPInputProps) => {
23
- const [inputValue, setValue] = useState(value || '')
24
-
25
22
  const classes = classNames([
26
23
  styles.wrapper,
27
24
  className
28
25
  ])
29
26
 
30
- const inputPlaceholders = Array.from({ length }, (_, i) => i + 1)
27
+ const inputs = Array.from({ length }, (_, i) => i + 1)
31
28
  .reduce<(number | string)[]>((acc, num, i) =>
32
29
  groupLength > 0 && i % groupLength === 0 && i !== 0
33
30
  ? [...acc, separator, num]
34
31
  : [...acc, num]
35
32
  , [])
36
33
 
37
- const getAdjustedIndex = (index: number) => inputPlaceholders
38
- .slice(0, index)
39
- .filter(placeholder => typeof placeholder !== 'string')
40
- .length
34
+ const focus = (direction: 'next' | 'prev', wrapper: HTMLElement | null, clear?: boolean) => {
35
+ const index = Number(wrapper?.dataset.active)
36
+ const nextIndex = direction === 'next' ? index + 1 : index - 1
37
+
38
+ const input = wrapper?.querySelector(`[data-index="${nextIndex}"]`)
39
+
40
+ if (input instanceof HTMLInputElement) {
41
+ input.focus()
42
+
43
+ if (clear) {
44
+ input.value = ''
45
+ }
46
+ }
47
+ }
48
+
49
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
50
+ const target = event.target as HTMLInputElement
51
+
52
+ if (event.key === 'Backspace' || event.key === 'Delete') {
53
+ if (!target.value) {
54
+ focus('prev', target.parentElement, true)
55
+ }
56
+ }
57
+
58
+ if (event.key === 'ArrowLeft') {
59
+ focus('prev', target.parentElement)
60
+ }
61
+
62
+ if (event.key === 'ArrowRight') {
63
+ focus('next', target.parentElement)
64
+ }
65
+ }
66
+
67
+ const handleInput = (event: React.FormEvent<HTMLInputElement>) => {
68
+ const target = event.target
69
+
70
+ if (!(target instanceof HTMLInputElement)) {
71
+ return
72
+ }
73
+
74
+ const index = Number(target.dataset.index)
75
+ const emptyIndex = Array.from(target.parentElement?.querySelectorAll('input') || [])
76
+ .findIndex(element => !element.value) + 1
77
+
78
+ if (emptyIndex !== 0 && emptyIndex < index) {
79
+ const emptyElement = target.parentElement?.querySelector(`[data-index="${emptyIndex}"]`)
80
+ const nextFocusElement = target.parentElement?.querySelector(`[data-index="${emptyIndex + 1}"]`)
81
+
82
+ if (emptyElement instanceof HTMLInputElement) {
83
+ emptyElement.value = target.value
84
+ }
85
+
86
+ if (nextFocusElement instanceof HTMLInputElement) {
87
+ nextFocusElement.focus()
88
+ }
89
+
90
+ target.value = ''
91
+
92
+ return
93
+ }
41
94
 
42
- const updateInput = (event: React.ChangeEvent<HTMLInputElement>) => {
43
- setValue(event.target.value)
44
- onChange?.(inputValue)
95
+ if (target.value) {
96
+ focus('next', target.parentElement)
97
+ }
98
+ }
99
+
100
+ const handlePaste = (event: ClipboardEvent) => {
101
+ event.preventDefault()
102
+
103
+ const target = event.target
104
+ const container = target instanceof HTMLInputElement ? target.parentElement : null
105
+
106
+ if (container) {
107
+ const paste = event.clipboardData?.getData('text') ?? ''
108
+ const nextIndex = Math.min(paste.length + 1, length)
109
+ const focusInput = container.querySelector(`[data-index="${nextIndex}"]`)
110
+
111
+ if (focusInput instanceof HTMLInputElement) {
112
+ focusInput.focus()
113
+ }
114
+
115
+ paste.split('').slice(0, length).forEach((char, i) => {
116
+ const input = container.querySelector(`[data-index="${i + 1}"]`)
117
+
118
+ if (input instanceof HTMLInputElement) {
119
+ input.value = char
120
+ }
121
+ })
122
+ }
123
+ }
124
+
125
+ const handleFocus = (event: Event) => {
126
+ const target = event.target
127
+
128
+ if (target instanceof HTMLInputElement) {
129
+ if (target.parentElement) {
130
+ target.parentElement.dataset.active = target.dataset.index
131
+ }
132
+
133
+ setTimeout(() => target.select(), 0)
134
+ }
135
+ }
136
+
137
+ const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
138
+ const target = event.target
139
+ const container = target instanceof HTMLInputElement ? target.parentElement : null
140
+
141
+ if (container) {
142
+ const newValue = Array.from(container.querySelectorAll('input') || [])
143
+ .map(input => input.value)
144
+ .join('')
145
+
146
+ onChange?.(newValue)
147
+ }
45
148
  }
46
149
 
47
150
  return (
48
151
  <div className={classes}>
49
152
  {label && (
50
153
  <label
51
- htmlFor={name}
154
+ htmlFor={`${name}-0`}
52
155
  className={styles.label}
53
156
  dangerouslySetInnerHTML={{ __html: label }}
54
157
  />
55
158
  )}
56
159
 
57
160
  <div className={styles['input-wrapper']}>
58
- <Input
59
- name={name || 'otp'}
60
- disabled={disabled}
61
- maxLength={length}
62
- required={true}
63
- value={inputValue}
64
- onChange={updateInput}
65
- {...rest}
66
- />
67
-
68
- <div className={styles.placeholders}>
69
- {inputPlaceholders.map((placeholder, index) => (
70
- <div
161
+ {inputs.map((input, index) =>
162
+ typeof input === 'string' ? (
163
+ <div className={styles.separator} key={index}>{input}</div>
164
+ ) : (
165
+ <Input
71
166
  key={index}
72
- className={typeof placeholder === 'string' ? styles.separator : styles.placeholder}
73
- data-active={getAdjustedIndex(index) === inputValue.length ? true : undefined}
74
- >
75
- {typeof placeholder === 'string'
76
- ? placeholder
77
- : inputValue[getAdjustedIndex(index)]}
78
- </div>
79
- ))}
80
- </div>
167
+ id={`${name}-${index}`}
168
+ className={styles.input}
169
+ type="text"
170
+ maxLength={1}
171
+ disabled={disabled}
172
+ inputMode="numeric"
173
+ autoComplete="one-time-code"
174
+ data-index={input}
175
+ aria-label={`OTP digit ${input + 1}`}
176
+ onKeyDown={handleKeyDown}
177
+ onKeyUp={handleKeyUp}
178
+ onFocus={handleFocus}
179
+ onInput={handleInput}
180
+ onPaste={handlePaste}
181
+ {...rest}
182
+ />
183
+ )
184
+ )}
81
185
  </div>
82
186
 
83
187
  {subText && (
@@ -1,85 +1,59 @@
1
- @use '../../scss/config.scss' as *;
2
-
3
- .wrapper {
4
- @include layout(flex, column, xs);
5
-
6
- .label {
7
- @include typography(primary-20);
8
- @include spacing(mb-xs);
9
- }
10
-
11
- .input-wrapper {
12
- @include position(relative);
13
-
14
- input {
15
- @include visibility(0);
16
- @include position(absolute, t0, l0);
17
- @include size(h40px);
18
- }
19
- }
20
-
21
- .placeholders {
22
- @include layout(flex);
23
- }
24
-
25
- .placeholder {
26
- @include layout(flex, center);
27
- @include border(top, primary-50);
28
- @include border(bottom, primary-50);
29
- @include border(right, primary-50);
30
- @include size(40px);
31
-
32
- &:first-child {
33
- @include border(primary-50);
34
- @include border-radius(left);
35
- }
36
-
37
- &:last-child {
38
- @include border(primary-50);
39
- @include border(left, 0);
40
- @include border-radius(right);
41
- }
42
-
43
- &[data-active="true"] {
44
- @include border(primary);
45
-
46
- &::before {
47
- @include size(w1px, h15px);
48
- @include background(primary);
49
-
50
- content: '';
51
- animation: flash 1s infinite forwards;
52
- }
53
- }
54
- }
55
-
56
- .separator {
57
- @include size(40px);
58
- @include layout(flex, center);
59
-
60
- + .placeholder {
61
- @include border(left, primary-50);
62
-
63
- &[data-active="true"] {
64
- @include border(left, primary);
65
- }
66
- }
67
- }
68
-
69
- .subtext {
70
- @include typography(md, primary-30);
71
- @include spacing(mt-xs);
72
- }
73
- }
74
-
75
- @keyframes flash {
76
- 0% {
77
- @include visibility(0);
78
- }
79
- 50% {
80
- @include visibility(1);
81
- }
82
- 100% {
83
- @include visibility(0);
84
- }
85
- }
1
+ @use '../../scss/config.scss' as *;
2
+
3
+ .wrapper {
4
+ @include layout(flex, column, xs);
5
+
6
+ .label {
7
+ @include typography(primary-20);
8
+ @include spacing(mb-xs);
9
+ }
10
+
11
+ .input-wrapper {
12
+ @include position(relative);
13
+ @include layout(flex);
14
+
15
+ input {
16
+ @include border-radius(none);
17
+ @include size(40px);
18
+ @include typography(center, default);
19
+
20
+ &:not(:first-child) {
21
+ @include border(left, 0);
22
+ }
23
+
24
+ &:first-child {
25
+ @include border-radius(left);
26
+ }
27
+
28
+ &:last-child {
29
+ @include border-radius(right);
30
+ }
31
+ }
32
+
33
+ .separator + input {
34
+ @include border(left, primary-50);
35
+ }
36
+ }
37
+
38
+ .separator {
39
+ @include size(40px);
40
+ @include layout(flex, center);
41
+ }
42
+
43
+ .subtext {
44
+ @include typography(md, primary-30);
45
+ @include spacing(mt-xs);
46
+ }
47
+ }
48
+
49
+ @keyframes flash {
50
+ 0% {
51
+ @include visibility(0);
52
+ }
53
+ 50% {
54
+ @include visibility(1);
55
+ }
56
+ 100% {
57
+ @include visibility(0);
58
+ }
59
+ }
@@ -139,20 +139,20 @@ const generatedPages = pages?.length
139
139
 
140
140
  Array.from(paginations).forEach(element => {
141
141
  const pagination = element as HTMLUListElement
142
- const totalPages = Number(pagination.dataset.totalPages)
143
142
  let currentPage = Number(pagination.dataset.currentPage)
144
143
 
145
144
  element.addEventListener('click', event => {
146
145
  const target = event.target as HTMLButtonElement
147
146
  const navigated = target.nodeName === 'BUTTON'
148
- && !target.dataset.active
147
+ && target.dataset.active !== 'true'
149
148
  && !target.disabled
150
149
 
151
150
  if (navigated) {
152
151
  const prevPageButton = element.querySelector('[data-page="prev"]') as HTMLButtonElement
153
152
  const nextPageButton = element.querySelector('[data-page="next"]') as HTMLButtonElement
154
- const currentPageButton = element.querySelector('[data-active]') as HTMLButtonElement
153
+ const currentPageButton = element.querySelector('[data-active="true"]') as HTMLButtonElement
155
154
  const previousPage = currentPage
155
+ const totalPages = Number(pagination.dataset.totalPages)
156
156
 
157
157
  const pageElements = Array
158
158
  .from(pagination.children)
@@ -35,16 +35,16 @@
35
35
  className
36
36
  ])
37
37
 
38
- const calculatedTotalPages = totalPages
38
+ const calculatedTotalPages = $derived(totalPages
39
39
  || pages?.length
40
- || 0
40
+ || 0)
41
41
 
42
- const generatedPages = pages?.length
42
+ const generatedPages = $derived(pages?.length
43
43
  ? pages
44
44
  : Array(totalPages || 0).fill(0).map((_, index) => ({
45
45
  ...(index === 0 && { active: true }),
46
46
  label: index + 1
47
- }))
47
+ })))
48
48
 
49
49
  const paginate = (to: string | number) => {
50
50
  const previousPage = calculatedCurrentPage
@@ -6,7 +6,7 @@
6
6
 
7
7
  list-style-type: none;
8
8
 
9
- &.primary [data-active] {
9
+ &.primary [data-active="true"] {
10
10
  @include typography(primary-70);
11
11
  @include background(primary-20);
12
12
  }
@@ -21,7 +21,7 @@
21
21
 
22
22
  cursor: pointer;
23
23
 
24
- &[data-active] {
24
+ &[data-active="true"] {
25
25
  @include background(primary);
26
26
  @include size(w20px);
27
27
  @include border-radius(lg);
@@ -41,7 +41,7 @@
41
41
  }
42
42
  }
43
43
 
44
- [data-active] {
44
+ [data-active="true"] {
45
45
  @include typography(primary);
46
46
  box-shadow: inset 0px 0px 0px 1px var(--w-color-primary);
47
47
  }