webcoreui 1.2.0 → 1.4.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 (82) hide show
  1. package/README.md +14 -6
  2. package/astro.d.ts +9 -0
  3. package/astro.js +6 -0
  4. package/components/Accordion/Accordion.astro +1 -0
  5. package/components/Accordion/Accordion.svelte +1 -1
  6. package/components/Accordion/Accordion.tsx +1 -1
  7. package/components/Accordion/accordion.ts +1 -0
  8. package/components/Avatar/Avatar.astro +4 -2
  9. package/components/Avatar/Avatar.svelte +6 -4
  10. package/components/Avatar/Avatar.tsx +4 -2
  11. package/components/Badge/Badge.astro +2 -0
  12. package/components/Badge/Badge.svelte +2 -0
  13. package/components/Badge/Badge.tsx +2 -0
  14. package/components/Badge/badge.module.scss +26 -0
  15. package/components/Badge/badge.ts +1 -0
  16. package/components/Checkbox/Checkbox.svelte +2 -0
  17. package/components/Checkbox/Checkbox.tsx +0 -2
  18. package/components/Checkbox/checkbox.ts +3 -1
  19. package/components/Collapsible/collapsible.ts +1 -1
  20. package/components/Counter/Counter.astro +164 -0
  21. package/components/Counter/Counter.svelte +141 -0
  22. package/components/Counter/Counter.tsx +161 -0
  23. package/components/Counter/counter.module.scss +155 -0
  24. package/components/Counter/counter.ts +21 -0
  25. package/components/DataTable/DataTable.astro +4 -4
  26. package/components/DataTable/DataTable.svelte +1 -1
  27. package/components/DataTable/DataTable.tsx +2 -2
  28. package/components/Icon/map.ts +2 -0
  29. package/components/Image/Image.astro +45 -0
  30. package/components/Image/Image.svelte +51 -0
  31. package/components/Image/Image.tsx +52 -0
  32. package/components/Image/image.module.scss +47 -0
  33. package/components/Image/image.ts +13 -0
  34. package/components/ImageLoader/ImageLoader.astro +82 -0
  35. package/components/ImageLoader/ImageLoader.svelte +72 -0
  36. package/components/ImageLoader/ImageLoader.tsx +82 -0
  37. package/components/ImageLoader/imageloader.module.scss +13 -0
  38. package/components/ImageLoader/imageloader.ts +6 -0
  39. package/components/Input/input.ts +2 -2
  40. package/components/List/List.astro +3 -0
  41. package/components/List/List.svelte +12 -9
  42. package/components/List/List.tsx +3 -0
  43. package/components/List/list.module.scss +5 -0
  44. package/components/List/list.ts +40 -39
  45. package/components/Menu/Menu.tsx +1 -1
  46. package/components/Pagination/Pagination.tsx +1 -1
  47. package/components/Pagination/pagination.module.scss +1 -0
  48. package/components/Popover/Popover.astro +28 -26
  49. package/components/Popover/Popover.svelte +2 -0
  50. package/components/Popover/Popover.tsx +2 -0
  51. package/components/Popover/popover.module.scss +10 -2
  52. package/components/Popover/popover.ts +17 -16
  53. package/components/Progress/Progress.astro +6 -2
  54. package/components/Progress/Progress.svelte +6 -2
  55. package/components/Progress/Progress.tsx +6 -2
  56. package/components/Progress/progress.module.scss +15 -0
  57. package/components/Progress/progress.ts +1 -0
  58. package/components/RangeSlider/RangeSlider.astro +5 -0
  59. package/components/RangeSlider/RangeSlider.svelte +3 -3
  60. package/components/RangeSlider/RangeSlider.tsx +1 -1
  61. package/components/RangeSlider/rangeslider.ts +1 -0
  62. package/components/Switch/Switch.svelte +2 -0
  63. package/components/Switch/Switch.tsx +0 -2
  64. package/components/Switch/switch.module.scss +1 -0
  65. package/components/Switch/switch.ts +3 -1
  66. package/components/Textarea/Textarea.svelte +2 -0
  67. package/components/Textarea/textarea.ts +7 -6
  68. package/components/ThemeSwitcher/themeswitcher.module.scss +1 -0
  69. package/components/ThemeSwitcher/themeswitcher.ts +1 -0
  70. package/icons/minus.svg +3 -0
  71. package/icons.d.ts +1 -0
  72. package/icons.js +1 -0
  73. package/index.d.ts +12 -5
  74. package/package.json +111 -109
  75. package/react.d.ts +9 -0
  76. package/react.js +6 -0
  77. package/scss/resets.scss +2 -0
  78. package/svelte.d.ts +9 -0
  79. package/svelte.js +6 -0
  80. package/utils/DOMUtils.ts +3 -3
  81. package/utils/modal.ts +2 -2
  82. package/utils/popover.ts +87 -46
@@ -0,0 +1,141 @@
1
+ <script lang="ts">
2
+ import type { SvelteCounterProps } from './counter'
3
+
4
+ import { classNames } from '../../utils/classNames'
5
+
6
+ import minusIcon from '../../icons/minus.svg?raw'
7
+ import plusIcon from '../../icons/plus.svg?raw'
8
+
9
+ import styles from './counter.module.scss'
10
+
11
+ let {
12
+ type = 'compact',
13
+ theme,
14
+ rounded,
15
+ minIcon,
16
+ maxIcon,
17
+ className,
18
+ width,
19
+ value = $bindable(0),
20
+ disabled,
21
+ onChange,
22
+ step = 1,
23
+ min,
24
+ max,
25
+ ...rest
26
+ }: SvelteCounterProps = $props()
27
+
28
+ const classes = classNames([
29
+ styles.counter,
30
+ styles[type],
31
+ theme && styles[theme],
32
+ rounded && styles.rounded,
33
+ className
34
+ ])
35
+
36
+ const subtractIcon = minIcon || minusIcon
37
+ const addIcon = maxIcon || plusIcon
38
+
39
+ const styleVariable = width
40
+ ? `--w-counter-width: ${width};`
41
+ : null
42
+
43
+ let intervalId: ReturnType<typeof setTimeout>
44
+ let timeoutId: ReturnType<typeof setTimeout>
45
+ let longPressDelay = 500
46
+ let isKeyDown = false
47
+
48
+ const updateValue = (isMin?: boolean) => {
49
+ const direction = isMin ? -1 : 1
50
+ const newValue = value + (direction * step)
51
+
52
+ if ((min !== undefined && newValue < min) || (max !== undefined && newValue > max)) {
53
+ return
54
+ }
55
+
56
+ value = newValue
57
+
58
+ onChange?.(newValue)
59
+ }
60
+
61
+ const startHold = (event: Event) => {
62
+ const target = event.currentTarget
63
+
64
+ if (target instanceof HTMLButtonElement) {
65
+ const isMin = target.dataset.id === 'w-counter-min'
66
+
67
+ updateValue(isMin)
68
+
69
+ timeoutId = setTimeout(function repeat() {
70
+ updateValue(isMin)
71
+
72
+ longPressDelay = Math.max(50, longPressDelay * 0.8)
73
+ intervalId = setTimeout(repeat, longPressDelay)
74
+ }, 500)
75
+ }
76
+ }
77
+
78
+ const stopHold = () => {
79
+ clearTimeout(timeoutId)
80
+ clearTimeout(intervalId)
81
+
82
+ isKeyDown = false
83
+ longPressDelay = 500
84
+ }
85
+
86
+ const handleKeyDown = (event: KeyboardEvent) => {
87
+ if (event.key === 'Enter' && !isKeyDown) {
88
+ event.preventDefault()
89
+ startHold(event)
90
+
91
+ isKeyDown = true
92
+ }
93
+ }
94
+
95
+ const handleKeyUp = (event: KeyboardEvent) => {
96
+ if (event.key === 'Enter') {
97
+ stopHold()
98
+ }
99
+ }
100
+ </script>
101
+
102
+ <div class={classes} style={styleVariable}>
103
+ <button
104
+ data-id="w-counter-min"
105
+ disabled={disabled}
106
+ onmousedown={startHold}
107
+ ontouchstart={startHold}
108
+ onmouseup={stopHold}
109
+ onmouseleave={stopHold}
110
+ ontouchend={stopHold}
111
+ ontouchcancel={stopHold}
112
+ onkeydown={handleKeyDown}
113
+ onkeyup={handleKeyUp}
114
+ >
115
+ {@html subtractIcon}
116
+ </button>
117
+ <input
118
+ bind:value={value}
119
+ type="number"
120
+ disabled={disabled}
121
+ step={step}
122
+ min={min}
123
+ max={max}
124
+ oninput={() => onChange?.(value)}
125
+ {...rest}
126
+ />
127
+ <button
128
+ data-id="w-counter-max"
129
+ disabled={disabled}
130
+ onmousedown={startHold}
131
+ ontouchstart={startHold}
132
+ onmouseup={stopHold}
133
+ onmouseleave={stopHold}
134
+ ontouchend={stopHold}
135
+ ontouchcancel={stopHold}
136
+ onkeydown={handleKeyDown}
137
+ onkeyup={handleKeyUp}
138
+ >
139
+ {@html addIcon}
140
+ </button>
141
+ </div>
@@ -0,0 +1,161 @@
1
+ import React, { useRef, useState } from 'react'
2
+ import type { ReactCounterProps } from './counter'
3
+
4
+ import { classNames } from '../../utils/classNames'
5
+
6
+ import minusIcon from '../../icons/minus.svg?raw'
7
+ import plusIcon from '../../icons/plus.svg?raw'
8
+
9
+ import styles from './counter.module.scss'
10
+
11
+ const Counter = ({
12
+ type = 'compact',
13
+ theme,
14
+ rounded,
15
+ minIcon,
16
+ maxIcon,
17
+ className,
18
+ width,
19
+ value = 0,
20
+ disabled,
21
+ onChange,
22
+ step = 1,
23
+ min,
24
+ max,
25
+ ...rest
26
+ }: ReactCounterProps) => {
27
+ const [inputValue, setInputValue] = useState(value)
28
+ const intervalId = useRef<ReturnType<typeof setTimeout> | null>(null)
29
+ const timeoutId = useRef<ReturnType<typeof setTimeout> | null>(null)
30
+ const longPressDelay = useRef(500)
31
+ const isKeyDown = useRef(false)
32
+
33
+ const classes = classNames([
34
+ styles.counter,
35
+ styles[type],
36
+ theme && styles[theme],
37
+ rounded && styles.rounded,
38
+ className
39
+ ])
40
+
41
+ const subtractIcon = minIcon || minusIcon
42
+ const addIcon = maxIcon || plusIcon
43
+
44
+ const styleVariable = width
45
+ ? { '--w-counter-width': width } as React.CSSProperties
46
+ : undefined
47
+
48
+ const updateValue = (isMin?: boolean) => {
49
+ setInputValue((prevValue: number) => {
50
+ const direction = isMin ? -1 : 1
51
+ const newValue = prevValue + (direction * step)
52
+
53
+ if ((min !== undefined && newValue < min) || (max !== undefined && newValue > max)) {
54
+ return prevValue
55
+ }
56
+
57
+ onChange?.(newValue)
58
+ return newValue
59
+ })
60
+ }
61
+
62
+ const startHold = (event: React.MouseEvent | React.TouchEvent | React.KeyboardEvent) => {
63
+ const target = event.currentTarget
64
+
65
+ if (target instanceof HTMLButtonElement) {
66
+ const isMin = target.dataset.id === 'w-counter-min'
67
+
68
+ updateValue(isMin)
69
+
70
+ timeoutId.current = setTimeout(function repeat() {
71
+ updateValue(isMin)
72
+
73
+ longPressDelay.current = Math.max(50, longPressDelay.current * 0.8)
74
+ intervalId.current = setTimeout(repeat, longPressDelay.current)
75
+ }, 500)
76
+ }
77
+ }
78
+
79
+ const stopHold = () => {
80
+ if (timeoutId.current) {
81
+ clearTimeout(timeoutId.current)
82
+ }
83
+
84
+ if (intervalId.current) {
85
+ clearTimeout(intervalId.current)
86
+ }
87
+
88
+ isKeyDown.current = false
89
+ longPressDelay.current = 500
90
+ }
91
+
92
+ const handleKeyDown = (event: React.KeyboardEvent) => {
93
+ if (event.key === 'Enter' && !isKeyDown.current) {
94
+ event.preventDefault()
95
+ startHold(event)
96
+
97
+ isKeyDown.current = true
98
+ }
99
+ }
100
+
101
+ const handleKeyUp = (event: React.KeyboardEvent) => {
102
+ if (event.key === 'Enter') {
103
+ stopHold()
104
+ }
105
+ }
106
+
107
+ const handleInput = (event: React.FormEvent) => {
108
+ const target = event.target
109
+
110
+ if (target instanceof HTMLInputElement) {
111
+ const newValue = Number(target.value)
112
+
113
+ setInputValue(newValue)
114
+ onChange?.(newValue)
115
+ }
116
+ }
117
+
118
+ return (
119
+ <div className={classes} style={styleVariable}>
120
+ <button
121
+ data-id="w-counter-min"
122
+ disabled={disabled}
123
+ onMouseDown={startHold}
124
+ onTouchStart={startHold}
125
+ onMouseUp={stopHold}
126
+ onMouseLeave={stopHold}
127
+ onTouchEnd={stopHold}
128
+ onTouchCancel={stopHold}
129
+ onKeyDown={handleKeyDown}
130
+ onKeyUp={handleKeyUp}
131
+ dangerouslySetInnerHTML={{ __html: subtractIcon }}
132
+ />
133
+ <input
134
+ value={inputValue}
135
+ type="number"
136
+ disabled={disabled}
137
+ step={step}
138
+ min={min}
139
+ max={max}
140
+ onInput={handleInput}
141
+ {...rest}
142
+ />
143
+ <button
144
+ data-id="w-counter-max"
145
+ disabled={disabled}
146
+ onMouseDown={startHold}
147
+ onTouchStart={startHold}
148
+ onMouseUp={stopHold}
149
+ onMouseLeave={stopHold}
150
+ onTouchEnd={stopHold}
151
+ onTouchCancel={stopHold}
152
+ onKeyDown={handleKeyDown}
153
+ onKeyUp={handleKeyUp}
154
+ dangerouslySetInnerHTML={{ __html: addIcon }}
155
+ />
156
+ </div>
157
+
158
+ )
159
+ }
160
+
161
+ export default Counter
@@ -0,0 +1,155 @@
1
+ @use '../../scss/config.scss' as *;
2
+
3
+ body {
4
+ --w-counter-width: 10ch;
5
+ }
6
+
7
+ .counter {
8
+ @include layout(inline-flex);
9
+ @include border(primary-50);
10
+ @include border-radius();
11
+
12
+ &.rounded {
13
+ @include border-radius(xl);
14
+ }
15
+
16
+ &.compact button {
17
+ @include border(0);
18
+ }
19
+
20
+ &.buttons {
21
+ button {
22
+ @include border(0);
23
+
24
+ &:first-child {
25
+ @include border(right, primary-50);
26
+ }
27
+
28
+ &:last-child {
29
+ @include border(left, primary-50);
30
+ }
31
+ }
32
+
33
+ &.info button {
34
+ border-color: var(--w-color-info);
35
+ }
36
+
37
+ &.success button {
38
+ border-color: var(--w-color-success);
39
+ }
40
+
41
+ &.warning button {
42
+ border-color: var(--w-color-warning);
43
+ }
44
+
45
+ &.alert button {
46
+ border-color: var(--w-color-alert);
47
+ }
48
+ }
49
+
50
+ &:not(.buttons) button {
51
+ @include border-radius();
52
+ }
53
+
54
+ &.separated {
55
+ @include layout(xs);
56
+ @include border(0);
57
+
58
+ input {
59
+ @include border(primary-50);
60
+ }
61
+
62
+ &.info input,
63
+ &.info button {
64
+ border-color: var(--w-color-info);
65
+ }
66
+
67
+ &.success input,
68
+ &.success button {
69
+ border-color: var(--w-color-success);
70
+ }
71
+
72
+ &.warning input,
73
+ &.warning button {
74
+ border-color: var(--w-color-warning);
75
+ }
76
+
77
+ &.alert input,
78
+ &.alert button {
79
+ border-color: var(--w-color-alert);
80
+ }
81
+ }
82
+
83
+ &.info:not(.separated) {
84
+ border-color: var(--w-color-info);
85
+ }
86
+
87
+ &.success:not(.separated) {
88
+ border-color: var(--w-color-success);
89
+ }
90
+
91
+ &.warning:not(.separated) {
92
+ border-color: var(--w-color-warning);
93
+ }
94
+
95
+ &.alert:not(.separated) {
96
+ border-color: var(--w-color-alert);
97
+ }
98
+
99
+ &.info button svg {
100
+ @include typography(info);
101
+ }
102
+
103
+ &.success button svg {
104
+ @include typography(success);
105
+ }
106
+
107
+ &.warning button svg {
108
+ @include typography(warning);
109
+ }
110
+
111
+ &.alert button svg {
112
+ @include typography(alert);
113
+ }
114
+
115
+ button {
116
+ @include background(transparent);
117
+ @include border(primary-50);
118
+ @include typography(primary);
119
+ @include spacing(p-xs);
120
+
121
+ cursor: pointer;
122
+
123
+ &[disabled] {
124
+ @include typography(primary-50);
125
+
126
+ cursor: default;
127
+ }
128
+
129
+ svg {
130
+ @include size(w18px, 'h100%');
131
+ }
132
+ }
133
+
134
+ input {
135
+ @include background(transparent);
136
+ @include border(0);
137
+ @include border-radius();
138
+ @include typography(primary, center);
139
+ @include spacing(p-xs);
140
+
141
+ flex: 1;
142
+ width: var(--w-counter-width);
143
+ appearance: none;
144
+ -moz-appearance: textfield;
145
+
146
+ &::-webkit-inner-spin-button,
147
+ &::-webkit-outer-spin-button {
148
+ appearance: none;
149
+ }
150
+
151
+ &[disabled] {
152
+ @include typography(primary-50);
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,21 @@
1
+ import type { ButtonProps } from '../Button/button'
2
+ import type { IconProps } from '../Icon/icon'
3
+
4
+ export type CounterProps = {
5
+ type?: 'compact' | 'buttons' | 'separated'
6
+ theme?: ButtonProps['theme']
7
+ rounded?: boolean
8
+ minIcon?: IconProps['type'] | string
9
+ maxIcon?: IconProps['type'] | string
10
+ width?: string
11
+ className?: string
12
+ [key: string]: any
13
+ }
14
+
15
+ export type SvelteCounterProps = {
16
+ onChange?: (value: number) => void
17
+ } & CounterProps
18
+
19
+ export type ReactCounterProps = {
20
+ onChange?: (value: number) => void
21
+ } & CounterProps
@@ -149,7 +149,7 @@ const hasPagination = data?.length && itemsPerPage
149
149
  {!!columnFilterItems?.length && (
150
150
  <tfoot data-hidden="true">
151
151
  <tr>
152
- <td colspan={data?.[0].length} class={styles['no-results']}>{noResultsLabel}</td>
152
+ <td colspan={headings?.length} class={styles['no-results']}>{noResultsLabel}</td>
153
153
  </tr>
154
154
  </tfoot>
155
155
  )}
@@ -251,9 +251,9 @@ const hasPagination = data?.length && itemsPerPage
251
251
  const sortedTableRows = Array.from(
252
252
  table?.querySelectorAll('tbody tr') as NodeListOf<HTMLTableRowElement>
253
253
  ).sort((a, b) => {
254
- let aValue: string | number = (a.querySelector(`[data-name=${sortBy}]`) as HTMLElement)
254
+ let aValue: string | number = (a.querySelector(`[data-name="${sortBy}"]`) as HTMLElement)
255
255
  ?.innerText.replace(/\s/g, '')
256
- let bValue: string | number = (b.querySelector(`[data-name=${sortBy}]`) as HTMLElement)
256
+ let bValue: string | number = (b.querySelector(`[data-name="${sortBy}"]`) as HTMLElement)
257
257
  ?.innerText.replace(/\s/g, '')
258
258
 
259
259
  if (!isNaN(aValue as any)) {
@@ -298,7 +298,7 @@ const hasPagination = data?.length && itemsPerPage
298
298
  return
299
299
  }
300
300
 
301
- const affectedTableCells = Array.from(table.querySelectorAll(`[data-name=${eventName}]`)) as HTMLElement[]
301
+ const affectedTableCells = Array.from(table.querySelectorAll(`[data-name="${eventName}"]`)) as HTMLElement[]
302
302
 
303
303
  const columnToggleListElement = Array.from(event.list.children)
304
304
  .find(child => (child as HTMLLIElement).dataset.name === event.name) as HTMLLIElement
@@ -250,7 +250,7 @@
250
250
  {#if columnFilterIndexes?.length && !filteredData.length}
251
251
  <tfoot>
252
252
  <tr>
253
- <td colspan={data?.[0].length} class={styles['no-results']}>
253
+ <td colspan={headings?.length} class={styles['no-results']}>
254
254
  {noResultsLabel}
255
255
  </td>
256
256
  </tr>
@@ -18,7 +18,6 @@ import styles from './datatable.module.scss'
18
18
 
19
19
  import type { ListEventType } from '../List/list'
20
20
 
21
- // eslint-disable-next-line complexity
22
21
  const DataTable = ({
23
22
  headings,
24
23
  filterPlaceholder = 'Filter entries',
@@ -38,6 +37,7 @@ const DataTable = ({
38
37
  id,
39
38
  onFilter,
40
39
  children
40
+ // eslint-disable-next-line complexity
41
41
  }: ReactDataTableProps) => {
42
42
  const [filteredData, setFilteredData] = useState<any>(data)
43
43
  const [toggledData, setToggledData] = useState(filteredData)
@@ -255,7 +255,7 @@ const DataTable = ({
255
255
  <tfoot>
256
256
  <tr>
257
257
  <td
258
- colSpan={data?.[0].length}
258
+ colSpan={headings?.length}
259
259
  className={styles['no-results']}
260
260
  >
261
261
  {noResultsLabel}
@@ -11,6 +11,7 @@ import Copy from '../../icons/copy.svg?raw'
11
11
  import Github from '../../icons/github.svg?raw'
12
12
  import Home from '../../icons/home.svg?raw'
13
13
  import Info from '../../icons/info.svg?raw'
14
+ import Minus from '../../icons/minus.svg?raw'
14
15
  import Moon from '../../icons/moon.svg?raw'
15
16
  import Order from '../../icons/order.svg?raw'
16
17
  import Plus from '../../icons/plus.svg?raw'
@@ -32,6 +33,7 @@ const iconMap = {
32
33
  'github': Github,
33
34
  'home': Home,
34
35
  'info': Info,
36
+ 'minus': Minus,
35
37
  'moon': Moon,
36
38
  'order': Order,
37
39
  'plus': Plus,
@@ -0,0 +1,45 @@
1
+ ---
2
+ import type { ImageProps } from './image'
3
+
4
+ import AspectRatio from '../AspectRatio/AspectRatio.astro'
5
+ import ConditionalWrapper from '../ConditionalWrapper/ConditionalWrapper.astro'
6
+
7
+ import styles from './image.module.scss'
8
+
9
+ interface Props extends ImageProps {}
10
+
11
+ const {
12
+ src,
13
+ alt,
14
+ width,
15
+ height,
16
+ lazy,
17
+ ratio,
18
+ center,
19
+ full,
20
+ rounded,
21
+ className,
22
+ ...rest
23
+ } = Astro.props
24
+
25
+ const classes = [
26
+ styles.img,
27
+ center && styles.center,
28
+ full && styles[full],
29
+ rounded && styles[rounded],
30
+ className
31
+ ]
32
+ ---
33
+
34
+ <ConditionalWrapper condition={!!ratio}>
35
+ <AspectRatio slot="wrapper" ratio={ratio || ''}>children</AspectRatio>
36
+ <img
37
+ {...rest}
38
+ src={src}
39
+ alt={alt || ''}
40
+ width={width}
41
+ height={height}
42
+ loading={lazy ? 'lazy' : null}
43
+ class:list={classes}
44
+ />
45
+ </ConditionalWrapper>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import type { ImageProps } from './image'
3
+
4
+ import AspectRatio from '../AspectRatio/AspectRatio.svelte'
5
+
6
+ import { classNames } from '../../utils/classNames'
7
+
8
+ import styles from './image.module.scss'
9
+
10
+ const {
11
+ src,
12
+ alt,
13
+ width,
14
+ height,
15
+ lazy,
16
+ ratio,
17
+ center,
18
+ full,
19
+ rounded,
20
+ className,
21
+ ...rest
22
+ }: ImageProps = $props()
23
+
24
+ const classes = classNames([
25
+ styles.img,
26
+ center && styles.center,
27
+ full && styles[full],
28
+ rounded && styles[rounded],
29
+ className
30
+ ])
31
+ </script>
32
+
33
+ {#snippet img()}
34
+ <img
35
+ {...rest}
36
+ src={src}
37
+ alt={alt || ''}
38
+ width={width}
39
+ height={height}
40
+ loading={lazy ? 'lazy' : null}
41
+ class={classes}
42
+ />
43
+ {/snippet}
44
+
45
+ {#if ratio}
46
+ <AspectRatio ratio={ratio}>
47
+ {@render img()}
48
+ </AspectRatio>
49
+ {:else}
50
+ {@render img()}
51
+ {/if}