free-astro-components 0.0.25 → 0.0.26

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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "liveServer.settings.port": 5501
3
+ }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A collection of free Astro components",
4
4
  "author": "Denis Ventura",
5
5
  "type": "module",
6
- "version": "0.0.25",
6
+ "version": "0.0.26",
7
7
  "exports": {
8
8
  ".": {
9
9
  "import": {
@@ -101,4 +101,8 @@ const checkboxClasses = [
101
101
  color: rgb(var(--ac-danger));
102
102
  }
103
103
  }
104
+
105
+ .ac-checkbox-label {
106
+ transition: color 0.3s ease-in-out;
107
+ }
104
108
  </style>
@@ -6,8 +6,6 @@ import Icon from './Icon.astro'
6
6
  interface Props {
7
7
  icon?: string
8
8
  label?: string
9
- placeholder?: string
10
- disabled?: boolean
11
9
  helperText?: string
12
10
  status?: 'default' | 'error' | 'success'
13
11
  class?: string
@@ -16,8 +14,6 @@ interface Props {
16
14
  const {
17
15
  icon,
18
16
  label,
19
- placeholder = '',
20
- disabled = false,
21
17
  helperText = '',
22
18
  status = 'default',
23
19
  class: className,
@@ -43,12 +39,7 @@ const inputClasses = ['ac-input', statusClasses, className]
43
39
  <Icon icon="search" class="ac-input-icon ac-input-icon--left" />
44
40
  )
45
41
  }
46
- <input
47
- class={inputClasses}
48
- placeholder={placeholder}
49
- disabled={disabled}
50
- {...props}
51
- />
42
+ <input class={inputClasses} {...props} />
52
43
  {
53
44
  icon && props.type !== 'password' && (
54
45
  <Icon icon={icon} class="ac-input-icon ac-input-icon--right" />
@@ -218,7 +209,8 @@ const inputClasses = ['ac-input', statusClasses, className]
218
209
  </style>
219
210
 
220
211
  <script>
221
- import { DOMLoaded, toggleInputPassword } from '../utils/utils.ts'
212
+ import { DOMLoaded } from '../utils/utils.ts'
213
+ import { toggleInputPassword } from '../utils/input'
222
214
 
223
215
  DOMLoaded(() => {
224
216
  const inputPasswordButtons = document.querySelectorAll(
@@ -0,0 +1,372 @@
1
+ ---
2
+ import '../css/main.css'
3
+ import Icon from './Icon.astro'
4
+
5
+ interface Props {
6
+ label?: string
7
+ placeholder?: string
8
+ helperText?: string
9
+ status?: 'default' | 'error' | 'success'
10
+ options?: { label: string; value: string | number; selected?: boolean }[]
11
+ class?: string
12
+ }
13
+
14
+ const {
15
+ label,
16
+ placeholder = '',
17
+ helperText,
18
+ status = 'default',
19
+ options = [
20
+ { label: 'Option 1', value: '1' },
21
+ { label: 'Option 2', value: '2' },
22
+ { label: 'Option 3', value: '3' },
23
+ ],
24
+ class: className,
25
+ ...props
26
+ } = Astro.props
27
+
28
+ const hasSelectedOption = options.some((option) => option.selected)
29
+
30
+ const statusClasses = {
31
+ default: '',
32
+ error: 'ac-select--error',
33
+ success: 'ac-select--success',
34
+ }[status]
35
+
36
+ const selectClasses = [
37
+ 'ac-select',
38
+ statusClasses,
39
+ className,
40
+ hasSelectedOption ? 'is-selected' : '',
41
+ ]
42
+ .filter(Boolean)
43
+ .join(' ')
44
+ ---
45
+
46
+ <label class="ac-select-wrapper">
47
+ {label && <span class="ac-select-label">{label}</span>}
48
+
49
+ <div>
50
+ <select class={selectClasses} {...props}>
51
+ {
52
+ hasSelectedOption ? (
53
+ <option disabled hidden>
54
+ {placeholder}
55
+ </option>
56
+ ) : (
57
+ <option disabled selected hidden>
58
+ {placeholder}
59
+ </option>
60
+ )
61
+ }
62
+
63
+ {
64
+ options.map((option) => (
65
+ <option value={option.value} selected={option.selected}>
66
+ {option.label}
67
+ </option>
68
+ ))
69
+ }
70
+ </select>
71
+
72
+ <Icon icon="chevron-down" class="ac-select-icon" />
73
+
74
+ <div class="ac-select-popover">
75
+ <div>
76
+ <ul class="ac-select-list">
77
+ {
78
+ options.map((option) => (
79
+ <li>
80
+ <button disabled class={option.selected ? 'is-selected' : ''}>
81
+ <span>{option.label}</span>
82
+ <Icon icon="check" />
83
+ </button>
84
+ </li>
85
+ ))
86
+ }
87
+ </ul>
88
+ </div>
89
+ </div>
90
+ </div>
91
+
92
+ {
93
+ helperText && (
94
+ <span class="ac-select-helper-text">
95
+ {status === 'error' && <Icon icon="warning" />}
96
+ {status === 'success' && <Icon icon="check-circle" />}
97
+ {helperText}
98
+ </span>
99
+ )
100
+ }
101
+ </label>
102
+
103
+ <style>
104
+ :root {
105
+ --ac-select-border-radius: var(--ac-rounded-xl);
106
+ --ac-select-padding: var(--ac-spacing-4);
107
+ --ac-select-height: var(--ac-spacing-12);
108
+ }
109
+
110
+ .ac-select-wrapper {
111
+ display: flex;
112
+ flex-direction: column;
113
+ font-family: var(--ac-font-sans);
114
+ gap: var(--ac-spacing-2);
115
+
116
+ > div {
117
+ position: relative;
118
+ }
119
+
120
+ &:has(.ac-select.is-open) .ac-select-icon {
121
+ transform: translateY(-50%) rotate(180deg);
122
+ }
123
+
124
+ &:has(.ac-select.is-open) .ac-select-popover {
125
+ grid-template-rows: 1fr;
126
+ }
127
+
128
+ &:has(.ac-select:disabled) {
129
+ opacity: var(--ac-disabled-opacity);
130
+ pointer-events: none;
131
+ }
132
+
133
+ &:has(.ac-select--error) .ac-select-helper-text {
134
+ color: rgb(var(--ac-danger));
135
+ }
136
+
137
+ &:has(.ac-select--success) .ac-select-helper-text {
138
+ color: rgb(var(--ac-success));
139
+ }
140
+ }
141
+
142
+ .ac-select {
143
+ appearance: none;
144
+ background-color: rgb(var(--ac-white));
145
+ background-image: none;
146
+ border-color: rgb(var(--ac-gray-100));
147
+ border-radius: var(--ac-select-border-radius);
148
+ border-width: var(--ac-border-2);
149
+ color: rgb(var(--ac-gray-300));
150
+ cursor: pointer;
151
+ font-size: var(--ac-text-base);
152
+ font-weight: var(--ac-font-normal);
153
+ height: var(--ac-select-height);
154
+ line-height: var(--ac-leading-normal);
155
+ padding-left: var(--ac-select-padding);
156
+ padding-right: var(--ac-select-padding);
157
+ transition: all 0.3s ease-in-out;
158
+ width: 100%;
159
+
160
+ &.is-selected {
161
+ color: rgb(var(--ac-dark));
162
+ }
163
+
164
+ &.is-open,
165
+ &.is-open:hover {
166
+ border-color: var(--ac-primary-hover);
167
+ }
168
+
169
+ &:hover {
170
+ border-color: rgb(var(--ac-gray-200));
171
+ }
172
+
173
+ &:focus,
174
+ &.ac-select--error:focus,
175
+ &:focus:hover {
176
+ border-color: var(--ac-primary-hover);
177
+ outline: none;
178
+ }
179
+
180
+ &:disabled {
181
+ background-color: rgb(var(--ac-gray-100));
182
+ }
183
+
184
+ &.ac-select--error,
185
+ &.ac-select--error:hover {
186
+ border-color: rgb(var(--ac-danger));
187
+ }
188
+ }
189
+
190
+ .ac-select-label {
191
+ color: rgb(var(--ac-gray-400));
192
+ font-size: var(--ac-text-sm);
193
+ }
194
+
195
+ .ac-select-helper-text {
196
+ align-items: center;
197
+ color: rgb(var(--ac-gray-300));
198
+ display: flex;
199
+ font-size: var(--ac-text-sm);
200
+ gap: var(--ac-spacing-1);
201
+
202
+ svg {
203
+ flex-shrink: 0;
204
+ height: var(--ac-spacing-4);
205
+ width: var(--ac-spacing-4);
206
+ }
207
+ }
208
+
209
+ .ac-select-icon {
210
+ color: rgb(var(--ac-gray-300));
211
+ position: absolute;
212
+ right: var(--ac-select-padding);
213
+ top: 50%;
214
+ transform-origin: center;
215
+ transform: translateY(-50%);
216
+ transition: all 0.3s ease-in-out;
217
+ }
218
+
219
+ .ac-select-popover {
220
+ display: grid;
221
+ grid-template-rows: 0fr;
222
+ left: 0;
223
+ position: absolute;
224
+ position: absolute;
225
+ right: 0;
226
+ top: calc(100% + var(--ac-spacing-1));
227
+ transition: grid-template-rows 0.3s ease-in-out;
228
+ z-index: 1;
229
+
230
+ > div {
231
+ background-color: rgb(var(--ac-white));
232
+ border-radius: var(--ac-select-border-radius);
233
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
234
+ list-style: none;
235
+ margin: 0;
236
+ overflow: hidden;
237
+ padding: 0;
238
+ }
239
+ }
240
+
241
+ .ac-select-list {
242
+ -ms-overflow-style: none;
243
+ border-color: rgb(var(--ac-gray-100));
244
+ border-radius: var(--ac-rounded-xl);
245
+ border-width: var(--ac-border-1);
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: var(--ac-spacing-1);
249
+ list-style: none;
250
+ margin: 0;
251
+ max-height: 14.188rem;
252
+ overflow-y: auto;
253
+ padding: var(--ac-spacing-1);
254
+ scrollbar-width: none;
255
+
256
+ > li > button {
257
+ align-items: center;
258
+ background-color: rgb(var(--ac-white));
259
+ border-radius: var(--ac-rounded-lg);
260
+ color: rgb(var(--ac-gray-400));
261
+ cursor: pointer;
262
+ display: flex;
263
+ font-size: var(--ac-text-sm);
264
+ justify-content: space-between;
265
+ padding: var(--ac-spacing-2) var(--ac-select-padding);
266
+ transition: all 0.3s ease-in-out;
267
+ width: 100%;
268
+
269
+ > svg {
270
+ color: rgb(var(--ac-primary));
271
+ opacity: 0;
272
+ transition: all 0.3s ease-in-out;
273
+ width: var(--ac-spacing-6);
274
+ }
275
+
276
+ &:hover {
277
+ color: var(--ac-primary-hover);
278
+ }
279
+
280
+ &:hover,
281
+ &:focus {
282
+ background-color: rgba(var(--ac-primary), 0.1);
283
+ }
284
+
285
+ &:focus {
286
+ outline: none;
287
+ }
288
+
289
+ &.is-selected {
290
+ background-color: rgba(var(--ac-primary), 0.1);
291
+ color: rgb(var(--ac-dark));
292
+
293
+ > svg {
294
+ opacity: 1;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ </style>
300
+
301
+ <script>
302
+ import { DOMLoaded } from '../utils/utils'
303
+ import {
304
+ closeSelect,
305
+ openSelect,
306
+ selectOption,
307
+ handleDocumentMousedown,
308
+ handleDocumentKeydown,
309
+ } from '../utils/select.ts'
310
+
311
+ DOMLoaded(() => {
312
+ const selects = document.querySelectorAll('.ac-select')
313
+
314
+ selects.forEach((item) => {
315
+ const select = item as HTMLSelectElement
316
+ const selectWrapper = select.closest('.ac-select-wrapper')
317
+ const popover = selectWrapper?.querySelector(
318
+ '.ac-select-popover'
319
+ ) as HTMLElement
320
+ const options = popover?.querySelectorAll(
321
+ '.ac-select-list li button'
322
+ ) as NodeListOf<HTMLButtonElement>
323
+
324
+ let isOpen = false
325
+
326
+ const setIsOpen = (value: boolean) => {
327
+ isOpen = value
328
+ }
329
+
330
+ select.addEventListener('mousedown', (event) =>
331
+ openSelect(event, options, select).then(() => {
332
+ setIsOpen(true)
333
+ })
334
+ )
335
+
336
+ select.addEventListener('keydown', (event) => {
337
+ if (
338
+ event.key === 'Enter' ||
339
+ event.code === 'Space' ||
340
+ event.key === 'ArrowDown' ||
341
+ event.key === 'ArrowUp'
342
+ )
343
+ openSelect(event, options, select).then(() => {
344
+ setIsOpen(true)
345
+ })
346
+ })
347
+
348
+ document.addEventListener('mousedown', (event) => {
349
+ handleDocumentMousedown(
350
+ event,
351
+ options,
352
+ select,
353
+ popover,
354
+ isOpen,
355
+ setIsOpen
356
+ )
357
+ })
358
+
359
+ document.addEventListener('keydown', (event) => {
360
+ handleDocumentKeydown(event, options, select, isOpen, setIsOpen)
361
+ })
362
+
363
+ options?.forEach((option, index) => {
364
+ option.addEventListener('click', () => {
365
+ selectOption(index, options, option, select)
366
+ closeSelect(options, select)
367
+ setIsOpen(false)
368
+ })
369
+ })
370
+ })
371
+ })
372
+ </script>
@@ -7,8 +7,6 @@ interface Props {
7
7
  icon?: string
8
8
  label?: string
9
9
  value?: string
10
- placeholder?: string
11
- disabled?: boolean
12
10
  helperText?: string
13
11
  status?: 'default' | 'error' | 'success'
14
12
  class?: string
@@ -18,8 +16,6 @@ const {
18
16
  icon,
19
17
  label,
20
18
  value,
21
- placeholder = '',
22
- disabled = false,
23
19
  helperText = '',
24
20
  status = 'default',
25
21
  class: className,
@@ -40,12 +36,7 @@ const inputClasses = ['ac-textarea', statusClasses, className]
40
36
  <label class="ac-textarea-wrapper">
41
37
  {label && <span class="ac-textarea-label">{label}</span>}
42
38
  <div>
43
- <textarea
44
- class={inputClasses}
45
- placeholder={placeholder}
46
- disabled={disabled}
47
- {...props}>{value}</textarea
48
- >
39
+ <textarea class={inputClasses} {...props}>{value}</textarea>
49
40
  {
50
41
  icon && (
51
42
  <Icon icon={icon} class="ac-textarea-icon ac-textarea-icon--right" />
package/src/index.js CHANGED
@@ -5,3 +5,4 @@ export { default as Radio } from './components/Radio.astro'
5
5
  export { default as Switch } from './components/Switch.astro'
6
6
  export { default as Input } from './components/Input.astro'
7
7
  export { default as Textarea } from './components/Textarea.astro'
8
+ export { default as Select } from './components/Select.astro'
@@ -2,6 +2,7 @@
2
2
  import Layout from '../layouts/Layout.astro'
3
3
  import Input from '../components/Input.astro'
4
4
  import Textarea from '../components/Textarea.astro'
5
+ import Select from '../components/Select.astro'
5
6
  ---
6
7
 
7
8
  <Layout title="Welcome to Astro.">
@@ -136,8 +137,61 @@ import Textarea from '../components/Textarea.astro'
136
137
  <Textarea
137
138
  icon="star"
138
139
  label="Textarea label"
139
- placeholder="Input with icon"
140
+ placeholder="Textarea with icon"
141
+ helperText="Helper text"
142
+ />
143
+ </div>
144
+
145
+ <h2>Select</h2>
146
+ <div class="controls-wrapper">
147
+ <Select
148
+ label="Select label"
149
+ placeholder="Select an option"
150
+ helperText="Helper text"
151
+ />
152
+ <Select
153
+ label="Select label"
154
+ placeholder="Select an option"
155
+ options={[
156
+ { label: 'Option 1', value: '1' },
157
+ { label: 'Option 2', value: '2', selected: true },
158
+ { label: 'Option 3', value: '3' },
159
+ ]}
160
+ helperText="Helper text"
161
+ />
162
+ <Select
163
+ label="Select label"
164
+ placeholder="Select an option"
140
165
  helperText="Helper text"
166
+ disabled
167
+ />
168
+ <Select
169
+ label="Select label"
170
+ placeholder="Select an option"
171
+ options={[
172
+ { label: 'Option 1', value: '1' },
173
+ { label: 'Option 2', value: '2', selected: true },
174
+ { label: 'Option 3', value: '3' },
175
+ ]}
176
+ helperText="Helper text"
177
+ disabled
178
+ />
179
+ <Select
180
+ label="Select label"
181
+ placeholder="Select an option"
182
+ helperText="Helper text"
183
+ status="error"
184
+ />
185
+ <Select
186
+ label="Select label"
187
+ placeholder="Select an option"
188
+ options={[
189
+ { label: 'Option 1', value: '1' },
190
+ { label: 'Option 2', value: '2', selected: true },
191
+ { label: 'Option 3', value: '3' },
192
+ ]}
193
+ helperText="Helper text"
194
+ status="success"
141
195
  />
142
196
  </div>
143
197
  </div>
@@ -148,7 +202,7 @@ import Textarea from '../components/Textarea.astro'
148
202
  /*@import '../css/preflight.css';*/
149
203
 
150
204
  .content {
151
- padding: var(--ac-spacing-8);
205
+ padding: var(--ac-spacing-8) var(--ac-spacing-8) 500px;
152
206
  }
153
207
 
154
208
  .controls-wrapper {
@@ -24,4 +24,8 @@ export const Input: Input
24
24
 
25
25
  // Textarea component
26
26
  export type Textarea = typeof import('../index.js').Textarea
27
- export const Textarea: Textarea
27
+ export const Textarea: Textarea
28
+
29
+ // Select component
30
+ export type Select = typeof import('../index.js').Select
31
+ export const Select: Select
@@ -0,0 +1,9 @@
1
+ export const toggleInputPassword = (button: Element, input: HTMLInputElement) => {
2
+ if (button.classList.contains('is-visible')) {
3
+ input.type = 'password'
4
+ button.classList.remove('is-visible')
5
+ } else {
6
+ input.type = 'text'
7
+ button.classList.add('is-visible')
8
+ }
9
+ }
@@ -0,0 +1,108 @@
1
+ export const openSelect = (
2
+ event: Event,
3
+ options: NodeListOf<HTMLButtonElement>,
4
+ select: HTMLSelectElement
5
+ ): Promise<void> => {
6
+ event.preventDefault()
7
+ select.classList.add('is-open')
8
+
9
+ return new Promise((resolve) => {
10
+ let hasSelectedOption = false
11
+
12
+ options.forEach((option: HTMLButtonElement) => {
13
+ option.removeAttribute('disabled')
14
+ if (option.classList.contains('is-selected')) {
15
+ hasSelectedOption = true
16
+ option.focus()
17
+ }
18
+ })
19
+
20
+ if (!hasSelectedOption && options.length > 0) {
21
+ options[0].focus()
22
+ }
23
+
24
+ setTimeout(() => resolve(), 0)
25
+ })
26
+ }
27
+
28
+ export const closeSelect = (
29
+ options: NodeListOf<HTMLButtonElement>,
30
+ select: HTMLSelectElement
31
+ ) => {
32
+ select.classList.remove('is-open')
33
+ options.forEach((option: HTMLButtonElement) => {
34
+ option.setAttribute('disabled', 'disabled')
35
+ })
36
+ }
37
+
38
+ export const selectOption = (
39
+ index: number,
40
+ options: NodeListOf<HTMLButtonElement>,
41
+ option: Element,
42
+ select: HTMLSelectElement
43
+ ) => {
44
+ select.selectedIndex = index + 1
45
+ select.classList.add('is-selected')
46
+ options.forEach((option: Element) => option.classList.remove('is-selected'))
47
+ option.classList.add('is-selected')
48
+ }
49
+
50
+ export const handleDocumentMousedown = (
51
+ event: MouseEvent,
52
+ options: NodeListOf<HTMLButtonElement>,
53
+ select: HTMLSelectElement,
54
+ popover: HTMLElement,
55
+ isOpen: boolean,
56
+ setIsOpen: (isOpen: boolean) => void
57
+ ) => {
58
+ if (!isOpen) return
59
+
60
+ const target = event.target as HTMLElement | null
61
+ const insideSelect = select.contains(target)
62
+ const insidePopover = popover?.contains(target)
63
+
64
+ if (!insideSelect && !insidePopover) {
65
+ closeSelect(options, select)
66
+ setIsOpen(false)
67
+ }
68
+ }
69
+
70
+ export const handleDocumentKeydown = (
71
+ event: KeyboardEvent,
72
+ options: NodeListOf<HTMLButtonElement>,
73
+ select: HTMLSelectElement,
74
+ isOpen: boolean,
75
+ setIsOpen: (isOpen: boolean) => void
76
+ ) => {
77
+ if (!isOpen) return
78
+
79
+ const activeElement = document.activeElement as HTMLElement
80
+ const focusedOption = Array.from(options).indexOf(
81
+ activeElement as HTMLButtonElement
82
+ )
83
+
84
+ if (event.key === 'Escape') {
85
+ closeSelect(options, select)
86
+ setIsOpen(false)
87
+ }
88
+
89
+ if (event.key === 'ArrowDown') {
90
+ event.preventDefault()
91
+ focusNextOption(focusedOption, options, 1)
92
+ }
93
+ if (event.key === 'ArrowUp') {
94
+ event.preventDefault()
95
+ focusNextOption(focusedOption, options, -1)
96
+ }
97
+ }
98
+
99
+ export const focusNextOption = (
100
+ currentIndex: number,
101
+ options: NodeListOf<HTMLButtonElement>,
102
+ direction: number
103
+ ) => {
104
+ if (currentIndex === -1) return
105
+ const nextIndex =
106
+ (currentIndex + direction + options.length) % options.length
107
+ options[nextIndex].focus()
108
+ }
@@ -4,14 +4,4 @@ export const DOMLoaded = (callback: () => void) => {
4
4
  } else {
5
5
  callback()
6
6
  }
7
- }
8
-
9
- export const toggleInputPassword = (button: Element, input: HTMLInputElement) => {
10
- if (button.classList.contains('is-visible')) {
11
- input.type = 'password'
12
- button.classList.remove('is-visible')
13
- } else {
14
- input.type = 'text'
15
- button.classList.add('is-visible')
16
- }
17
7
  }