rizzo-css 0.0.1 → 0.0.3
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/README.md +13 -7
- package/bin/rizzo-css.js +303 -0
- package/dist/rizzo.min.css +1 -1
- package/package.json +13 -4
- package/scaffold/astro/Accordion.astro +178 -0
- package/scaffold/astro/Alert.astro +131 -0
- package/scaffold/astro/Avatar.astro +59 -0
- package/scaffold/astro/Badge.astro +24 -0
- package/scaffold/astro/Breadcrumb.astro +61 -0
- package/scaffold/astro/Button.astro +3 -0
- package/scaffold/astro/Card.astro +18 -0
- package/scaffold/astro/Checkbox.astro +38 -0
- package/scaffold/astro/CopyToClipboard.astro +199 -0
- package/scaffold/astro/Divider.astro +37 -0
- package/scaffold/astro/Dropdown.astro +807 -0
- package/scaffold/astro/FormGroup.astro +51 -0
- package/scaffold/astro/Input.astro +59 -0
- package/scaffold/astro/Modal.astro +212 -0
- package/scaffold/astro/Pagination.astro +240 -0
- package/scaffold/astro/ProgressBar.astro +65 -0
- package/scaffold/astro/Radio.astro +38 -0
- package/scaffold/astro/Select.astro +49 -0
- package/scaffold/astro/Spinner.astro +30 -0
- package/scaffold/astro/Table.astro +181 -0
- package/scaffold/astro/Tabs.astro +223 -0
- package/scaffold/astro/Textarea.astro +58 -0
- package/scaffold/astro/Toast.astro +30 -0
- package/scaffold/astro/Tooltip.astro +32 -0
- package/scaffold/astro/icons/Brush.astro +11 -0
- package/scaffold/astro/icons/Cake.astro +12 -0
- package/scaffold/astro/icons/Check.astro +30 -0
- package/scaffold/astro/icons/Cherry.astro +12 -0
- package/scaffold/astro/icons/ChevronDown.astro +30 -0
- package/scaffold/astro/icons/Circle.astro +30 -0
- package/scaffold/astro/icons/Close.astro +31 -0
- package/scaffold/astro/icons/Copy.astro +31 -0
- package/scaffold/astro/icons/Eye.astro +31 -0
- package/scaffold/astro/icons/Filter.astro +30 -0
- package/scaffold/astro/icons/Flame.astro +29 -0
- package/scaffold/astro/icons/Flower.astro +12 -0
- package/scaffold/astro/icons/Gear.astro +31 -0
- package/scaffold/astro/icons/Heart.astro +29 -0
- package/scaffold/astro/icons/IceCream.astro +32 -0
- package/scaffold/astro/icons/Leaf.astro +30 -0
- package/scaffold/astro/icons/Lemon.astro +12 -0
- package/scaffold/astro/icons/Moon.astro +30 -0
- package/scaffold/astro/icons/Owl.astro +35 -0
- package/scaffold/astro/icons/Palette.astro +34 -0
- package/scaffold/astro/icons/Rainbow.astro +32 -0
- package/scaffold/astro/icons/Search.astro +31 -0
- package/scaffold/astro/icons/Shield.astro +29 -0
- package/scaffold/astro/icons/Snowflake.astro +35 -0
- package/scaffold/astro/icons/Sort.astro +31 -0
- package/scaffold/astro/icons/Sun.astro +30 -0
- package/scaffold/astro/icons/Sunset.astro +11 -0
- package/scaffold/astro/icons/Zap.astro +10 -0
- package/scaffold/astro/icons/devicons/Astro.astro +54 -0
- package/scaffold/astro/icons/devicons/Bash.astro +35 -0
- package/scaffold/astro/icons/devicons/Css3.astro +30 -0
- package/scaffold/astro/icons/devicons/Git.astro +25 -0
- package/scaffold/astro/icons/devicons/Html5.astro +28 -0
- package/scaffold/astro/icons/devicons/Javascript.astro +26 -0
- package/scaffold/astro/icons/devicons/Nodejs.astro +48 -0
- package/scaffold/astro/icons/devicons/Plaintext.astro +34 -0
- package/scaffold/astro/icons/devicons/React.astro +28 -0
- package/scaffold/astro/icons/devicons/Svelte.astro +26 -0
- package/scaffold/astro/icons/devicons/Vue.astro +27 -0
- package/scaffold/svelte/.gitkeep +0 -0
- package/scaffold/svelte/Accordion.svelte +128 -0
- package/scaffold/svelte/Alert.svelte +79 -0
- package/scaffold/svelte/Avatar.svelte +39 -0
- package/scaffold/svelte/Badge.svelte +31 -0
- package/scaffold/svelte/Breadcrumb.svelte +46 -0
- package/scaffold/svelte/Button.svelte +23 -0
- package/scaffold/svelte/Card.svelte +14 -0
- package/scaffold/svelte/Checkbox.svelte +37 -0
- package/scaffold/svelte/CopyToClipboard.svelte +76 -0
- package/scaffold/svelte/Divider.svelte +28 -0
- package/scaffold/svelte/Dropdown.svelte +237 -0
- package/scaffold/svelte/FormGroup.svelte +41 -0
- package/scaffold/svelte/Input.svelte +57 -0
- package/scaffold/svelte/Modal.svelte +152 -0
- package/scaffold/svelte/Pagination.svelte +93 -0
- package/scaffold/svelte/ProgressBar.svelte +56 -0
- package/scaffold/svelte/Radio.svelte +38 -0
- package/scaffold/svelte/Select.svelte +47 -0
- package/scaffold/svelte/Spinner.svelte +14 -0
- package/scaffold/svelte/Table.svelte +155 -0
- package/scaffold/svelte/Tabs.svelte +109 -0
- package/scaffold/svelte/Textarea.svelte +57 -0
- package/scaffold/svelte/Toast.svelte +30 -0
- package/scaffold/svelte/Tooltip.svelte +19 -0
- package/scaffold/svelte/index.ts +33 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
label?: string;
|
|
4
|
+
labelFor?: string;
|
|
5
|
+
required?: boolean;
|
|
6
|
+
help?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
success?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
label,
|
|
14
|
+
labelFor,
|
|
15
|
+
required = false,
|
|
16
|
+
help,
|
|
17
|
+
error,
|
|
18
|
+
success,
|
|
19
|
+
class: className = '',
|
|
20
|
+
} = Astro.props;
|
|
21
|
+
|
|
22
|
+
const errorId = labelFor && error ? `${labelFor}-error` : undefined;
|
|
23
|
+
const helpId = labelFor && help ? `${labelFor}-help` : undefined;
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
<div class={`form-group ${className}`}>
|
|
27
|
+
{label && (
|
|
28
|
+
<label
|
|
29
|
+
for={labelFor}
|
|
30
|
+
class={`form-group__label ${required ? 'required' : ''}`}
|
|
31
|
+
>
|
|
32
|
+
{label}
|
|
33
|
+
</label>
|
|
34
|
+
)}
|
|
35
|
+
<slot />
|
|
36
|
+
{help && (
|
|
37
|
+
<span id={helpId} class="form-group__help">
|
|
38
|
+
{help}
|
|
39
|
+
</span>
|
|
40
|
+
)}
|
|
41
|
+
{error && (
|
|
42
|
+
<span id={errorId} class="form-error" role="alert">
|
|
43
|
+
{error}
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
{success && (
|
|
47
|
+
<span class="form-success" role="status">
|
|
48
|
+
{success}
|
|
49
|
+
</span>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local' | 'month' | 'week';
|
|
4
|
+
id?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
value?: string;
|
|
7
|
+
placeholder?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
readonly?: boolean;
|
|
11
|
+
autocomplete?: string;
|
|
12
|
+
size?: 'sm' | 'md' | 'lg';
|
|
13
|
+
error?: boolean;
|
|
14
|
+
success?: boolean;
|
|
15
|
+
class?: string;
|
|
16
|
+
ariaDescribedby?: string;
|
|
17
|
+
ariaInvalid?: boolean | 'true' | 'false';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
type = 'text',
|
|
22
|
+
id,
|
|
23
|
+
name,
|
|
24
|
+
value,
|
|
25
|
+
placeholder,
|
|
26
|
+
required = false,
|
|
27
|
+
disabled = false,
|
|
28
|
+
readonly = false,
|
|
29
|
+
autocomplete,
|
|
30
|
+
size = 'md',
|
|
31
|
+
error = false,
|
|
32
|
+
success = false,
|
|
33
|
+
class: className = '',
|
|
34
|
+
ariaDescribedby,
|
|
35
|
+
ariaInvalid,
|
|
36
|
+
} = Astro.props;
|
|
37
|
+
|
|
38
|
+
const sizeClass = size !== 'md' ? `form-input--${size}` : '';
|
|
39
|
+
const errorClass = error ? 'form-input--error' : '';
|
|
40
|
+
const successClass = success ? 'form-input--success' : '';
|
|
41
|
+
const classes = `form-input ${sizeClass} ${errorClass} ${successClass} ${className}`.trim();
|
|
42
|
+
|
|
43
|
+
const invalid = error || ariaInvalid === true || ariaInvalid === 'true';
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
<input
|
|
47
|
+
type={type}
|
|
48
|
+
id={id}
|
|
49
|
+
name={name}
|
|
50
|
+
value={value}
|
|
51
|
+
placeholder={placeholder}
|
|
52
|
+
required={required}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
readonly={readonly}
|
|
55
|
+
autocomplete={autocomplete}
|
|
56
|
+
class={classes}
|
|
57
|
+
aria-invalid={invalid ? 'true' : 'false'}
|
|
58
|
+
aria-describedby={ariaDescribedby}
|
|
59
|
+
/>
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Close from './icons/Close.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
id?: string;
|
|
6
|
+
title?: string;
|
|
7
|
+
size?: 'sm' | 'md' | 'lg';
|
|
8
|
+
open?: boolean;
|
|
9
|
+
closeOnOverlayClick?: boolean;
|
|
10
|
+
closeOnEscape?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
id,
|
|
16
|
+
title = 'Modal',
|
|
17
|
+
size = 'md',
|
|
18
|
+
open = false,
|
|
19
|
+
closeOnOverlayClick = true,
|
|
20
|
+
closeOnEscape = true,
|
|
21
|
+
class: className = '',
|
|
22
|
+
} = Astro.props;
|
|
23
|
+
|
|
24
|
+
const modalId = id || `modal-${Math.random().toString(36).substr(2, 9)}`;
|
|
25
|
+
const sizeClass = size !== 'md' ? `modal--${size}` : '';
|
|
26
|
+
const classes = `modal ${sizeClass} ${className}`.trim();
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
class="modal__overlay"
|
|
31
|
+
data-modal-overlay
|
|
32
|
+
aria-hidden={open ? 'false' : 'true'}
|
|
33
|
+
id={`${modalId}-overlay`}
|
|
34
|
+
></div>
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
class={classes}
|
|
38
|
+
role="dialog"
|
|
39
|
+
aria-modal="true"
|
|
40
|
+
aria-labelledby={`${modalId}-title`}
|
|
41
|
+
aria-hidden={open ? 'false' : 'true'}
|
|
42
|
+
id={modalId}
|
|
43
|
+
data-modal
|
|
44
|
+
>
|
|
45
|
+
<div class="modal__header">
|
|
46
|
+
<h2 id={`${modalId}-title`} class="modal__title">
|
|
47
|
+
{title}
|
|
48
|
+
</h2>
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
class="modal__close"
|
|
52
|
+
aria-label="Close modal"
|
|
53
|
+
data-modal-close
|
|
54
|
+
>
|
|
55
|
+
<Close width={20} height={20} />
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="modal__body">
|
|
60
|
+
<slot />
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div class="modal__footer">
|
|
64
|
+
<slot name="footer" />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<script define:vars={{ modalId, closeOnEscape, closeOnOverlayClick, open }}>
|
|
69
|
+
(function initModal() {
|
|
70
|
+
// Wait for DOM to be ready
|
|
71
|
+
const init = () => {
|
|
72
|
+
const modal = document.querySelector(`#${modalId}`);
|
|
73
|
+
if (!modal) {
|
|
74
|
+
// Retry if modal not found yet
|
|
75
|
+
if (document.readyState === 'loading') {
|
|
76
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const overlay = document.querySelector(`#${modalId}-overlay`);
|
|
83
|
+
const closeBtn = modal.querySelector('[data-modal-close]');
|
|
84
|
+
const title = modal.querySelector(`#${modalId}-title`);
|
|
85
|
+
|
|
86
|
+
if (!overlay || !closeBtn) return;
|
|
87
|
+
|
|
88
|
+
// Ensure modal starts closed unless explicitly opened
|
|
89
|
+
if (!open) {
|
|
90
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
91
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
92
|
+
modal.removeAttribute('data-open');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Get focusable elements within modal
|
|
96
|
+
const getFocusableElements = (container) => {
|
|
97
|
+
const focusableSelectors = [
|
|
98
|
+
'button:not([disabled])',
|
|
99
|
+
'a[href]',
|
|
100
|
+
'input:not([disabled])',
|
|
101
|
+
'select:not([disabled])',
|
|
102
|
+
'textarea:not([disabled])',
|
|
103
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
104
|
+
].join(', ');
|
|
105
|
+
return Array.from(container.querySelectorAll(focusableSelectors));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let previousActiveElement = null;
|
|
109
|
+
let focusTrapHandler = null;
|
|
110
|
+
|
|
111
|
+
// Open modal
|
|
112
|
+
const openModal = () => {
|
|
113
|
+
previousActiveElement = document.activeElement;
|
|
114
|
+
modal.setAttribute('aria-hidden', 'false');
|
|
115
|
+
overlay.setAttribute('aria-hidden', 'false');
|
|
116
|
+
modal.setAttribute('data-open', 'true');
|
|
117
|
+
|
|
118
|
+
// Focus first focusable element or close button
|
|
119
|
+
const focusableElements = getFocusableElements(modal);
|
|
120
|
+
const firstFocusable = focusableElements.length > 0 ? focusableElements[0] : closeBtn;
|
|
121
|
+
if (firstFocusable) {
|
|
122
|
+
setTimeout(() => firstFocusable.focus(), 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Add focus trap
|
|
126
|
+
focusTrapHandler = (e) => {
|
|
127
|
+
if (modal.getAttribute('data-open') !== 'true') return;
|
|
128
|
+
|
|
129
|
+
if (e.key === 'Escape' && closeOnEscape) {
|
|
130
|
+
e.preventDefault();
|
|
131
|
+
closeModal();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Focus trap: Tab key
|
|
136
|
+
if (e.key === 'Tab') {
|
|
137
|
+
const focusableElements = getFocusableElements(modal);
|
|
138
|
+
if (focusableElements.length === 0) return;
|
|
139
|
+
|
|
140
|
+
const firstElement = focusableElements[0];
|
|
141
|
+
const lastElement = focusableElements[focusableElements.length - 1];
|
|
142
|
+
const activeElement = document.activeElement;
|
|
143
|
+
|
|
144
|
+
if (e.shiftKey) {
|
|
145
|
+
// Shift + Tab: move backwards
|
|
146
|
+
if (activeElement === firstElement || !modal.contains(activeElement)) {
|
|
147
|
+
e.preventDefault();
|
|
148
|
+
lastElement.focus();
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// Tab: move forwards
|
|
152
|
+
if (activeElement === lastElement || !modal.contains(activeElement)) {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
firstElement.focus();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
document.addEventListener('keydown', focusTrapHandler);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Close modal
|
|
164
|
+
const closeModal = () => {
|
|
165
|
+
modal.setAttribute('aria-hidden', 'true');
|
|
166
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
167
|
+
modal.removeAttribute('data-open');
|
|
168
|
+
|
|
169
|
+
// Remove focus trap
|
|
170
|
+
if (focusTrapHandler) {
|
|
171
|
+
document.removeEventListener('keydown', focusTrapHandler);
|
|
172
|
+
focusTrapHandler = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Return focus to previous element
|
|
176
|
+
if (previousActiveElement) {
|
|
177
|
+
previousActiveElement.focus();
|
|
178
|
+
previousActiveElement = null;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// Event listeners
|
|
183
|
+
closeBtn.addEventListener('click', closeModal);
|
|
184
|
+
|
|
185
|
+
if (closeOnOverlayClick) {
|
|
186
|
+
overlay.addEventListener('click', (e) => {
|
|
187
|
+
if (e.target === overlay) {
|
|
188
|
+
closeModal();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Initialize if open prop is true
|
|
194
|
+
if (open) {
|
|
195
|
+
setTimeout(() => openModal(), 0);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Expose methods globally
|
|
199
|
+
// Convert hyphens to underscores for valid JavaScript identifiers
|
|
200
|
+
const modalIdAttr = modal.id.replace(/-/g, '_');
|
|
201
|
+
window[`openModal_${modalIdAttr}`] = openModal;
|
|
202
|
+
window[`closeModal_${modalIdAttr}`] = closeModal;
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Start initialization
|
|
206
|
+
if (document.readyState === 'loading') {
|
|
207
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
208
|
+
} else {
|
|
209
|
+
init();
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
</script>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
currentPage: number;
|
|
4
|
+
totalPages: number;
|
|
5
|
+
hrefTemplate?: string;
|
|
6
|
+
showFirstLast?: boolean;
|
|
7
|
+
maxVisible?: number;
|
|
8
|
+
/** When true, add data attributes and script so clicking updates hash and current page (for demos) */
|
|
9
|
+
syncHash?: boolean;
|
|
10
|
+
class?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
currentPage,
|
|
15
|
+
totalPages,
|
|
16
|
+
hrefTemplate = '?page={page}',
|
|
17
|
+
showFirstLast = true,
|
|
18
|
+
maxVisible = 5,
|
|
19
|
+
syncHash = false,
|
|
20
|
+
class: className = '',
|
|
21
|
+
} = Astro.props;
|
|
22
|
+
|
|
23
|
+
const classes = `pagination ${className}`.trim();
|
|
24
|
+
const hashMatch = typeof hrefTemplate === 'string' && hrefTemplate.startsWith('#');
|
|
25
|
+
const dataSync = syncHash && hashMatch ? { 'data-pagination-sync': 'true', 'data-total-pages': String(totalPages), 'data-href-template': hrefTemplate } : {};
|
|
26
|
+
|
|
27
|
+
function buildHref(page: number): string {
|
|
28
|
+
return hrefTemplate.replace(/\{page\}/g, String(page));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build array of page numbers and 'ellipsis' for display */
|
|
32
|
+
function getPageItems(total: number, current: number, maxVisible: number): (number | 'ellipsis')[] {
|
|
33
|
+
if (total <= 1) return [];
|
|
34
|
+
if (total <= maxVisible) {
|
|
35
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
36
|
+
}
|
|
37
|
+
const items: (number | 'ellipsis')[] = [1];
|
|
38
|
+
const delta = Math.max(0, Math.floor((maxVisible - 2) / 2));
|
|
39
|
+
const start = Math.max(2, current - delta);
|
|
40
|
+
const end = Math.min(total - 1, current + delta);
|
|
41
|
+
if (start > 2) items.push('ellipsis');
|
|
42
|
+
for (let p = start; p <= end; p++) {
|
|
43
|
+
if (p !== 1 && p !== total) items.push(p);
|
|
44
|
+
}
|
|
45
|
+
if (end < total - 1) items.push('ellipsis');
|
|
46
|
+
if (total > 1) items.push(total);
|
|
47
|
+
return items;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const pageItems = getPageItems(totalPages, currentPage, maxVisible);
|
|
51
|
+
const hasPrev = currentPage > 1;
|
|
52
|
+
const hasNext = currentPage < totalPages;
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
<nav class={classes} aria-label="Pagination" {...dataSync}>
|
|
56
|
+
<ul class="pagination__list">
|
|
57
|
+
{showFirstLast && totalPages > 1 && (
|
|
58
|
+
<li class="pagination__item">
|
|
59
|
+
{hasPrev ? (
|
|
60
|
+
<a class="pagination__link pagination__link--prev" href={buildHref(1)} aria-label="First page" data-pagination-role="first">
|
|
61
|
+
First
|
|
62
|
+
</a>
|
|
63
|
+
) : (
|
|
64
|
+
<span class="pagination__link pagination__link--prev pagination__link--disabled" aria-disabled="true" data-pagination-role="first">
|
|
65
|
+
First
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</li>
|
|
69
|
+
)}
|
|
70
|
+
<li class="pagination__item">
|
|
71
|
+
{hasPrev ? (
|
|
72
|
+
<a class="pagination__link pagination__link--prev" href={buildHref(currentPage - 1)} aria-label="Previous page" data-pagination-role="prev">
|
|
73
|
+
Previous
|
|
74
|
+
</a>
|
|
75
|
+
) : (
|
|
76
|
+
<span class="pagination__link pagination__link--prev pagination__link--disabled" aria-disabled="true" data-pagination-role="prev">
|
|
77
|
+
Previous
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
</li>
|
|
81
|
+
{pageItems.map((item) => (
|
|
82
|
+
<li class="pagination__item">
|
|
83
|
+
{item === 'ellipsis' ? (
|
|
84
|
+
<span class="pagination__ellipsis" aria-hidden="true">
|
|
85
|
+
…
|
|
86
|
+
</span>
|
|
87
|
+
) : item === currentPage ? (
|
|
88
|
+
<span class="pagination__link pagination__link--current" aria-current="page" data-page={String(item)}>
|
|
89
|
+
{item}
|
|
90
|
+
</span>
|
|
91
|
+
) : (
|
|
92
|
+
<a class="pagination__link" href={buildHref(item)} aria-label={`Page ${item}`} data-page={String(item)}>
|
|
93
|
+
{item}
|
|
94
|
+
</a>
|
|
95
|
+
)}
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
<li class="pagination__item">
|
|
99
|
+
{hasNext ? (
|
|
100
|
+
<a class="pagination__link pagination__link--next" href={buildHref(currentPage + 1)} aria-label="Next page" data-pagination-role="next">
|
|
101
|
+
Next
|
|
102
|
+
</a>
|
|
103
|
+
) : (
|
|
104
|
+
<span class="pagination__link pagination__link--next pagination__link--disabled" aria-disabled="true" data-pagination-role="next">
|
|
105
|
+
Next
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</li>
|
|
109
|
+
{showFirstLast && totalPages > 1 && (
|
|
110
|
+
<li class="pagination__item">
|
|
111
|
+
{hasNext ? (
|
|
112
|
+
<a class="pagination__link pagination__link--next" href={buildHref(totalPages)} aria-label="Last page" data-pagination-role="last">
|
|
113
|
+
Last
|
|
114
|
+
</a>
|
|
115
|
+
) : (
|
|
116
|
+
<span class="pagination__link pagination__link--next pagination__link--disabled" aria-disabled="true" data-pagination-role="last">
|
|
117
|
+
Last
|
|
118
|
+
</span>
|
|
119
|
+
)}
|
|
120
|
+
</li>
|
|
121
|
+
)}
|
|
122
|
+
</ul>
|
|
123
|
+
</nav>
|
|
124
|
+
|
|
125
|
+
{syncHash && hashMatch && (
|
|
126
|
+
<script is:inline>
|
|
127
|
+
(function() {
|
|
128
|
+
function getHref(nav, page) {
|
|
129
|
+
var t = nav.getAttribute('data-href-template') || '#page-{page}';
|
|
130
|
+
return t.replace(/\{page\}/g, String(page));
|
|
131
|
+
}
|
|
132
|
+
function getRoleElement(nav, role) {
|
|
133
|
+
var candidates = nav.querySelectorAll('[data-pagination-role="' + role + '"]');
|
|
134
|
+
for (var i = 0; i < candidates.length; i++) {
|
|
135
|
+
if (candidates[i].closest && candidates[i].closest('[data-pagination-sync="true"]') === nav) return candidates[i];
|
|
136
|
+
}
|
|
137
|
+
return candidates[0] || null;
|
|
138
|
+
}
|
|
139
|
+
function setCurrentPage(nav, pageNum) {
|
|
140
|
+
var total = parseInt(nav.getAttribute('data-total-pages'), 10) || 1;
|
|
141
|
+
pageNum = Math.max(1, Math.min(pageNum, total));
|
|
142
|
+
|
|
143
|
+
Array.from(nav.querySelectorAll('[data-page]')).forEach(function(el) {
|
|
144
|
+
var page = el.getAttribute('data-page');
|
|
145
|
+
if (page === 'ellipsis') return;
|
|
146
|
+
var num = parseInt(page, 10);
|
|
147
|
+
if (isNaN(num)) return;
|
|
148
|
+
var isCurrent = num === pageNum;
|
|
149
|
+
var parent = el.parentNode;
|
|
150
|
+
if (isCurrent && el.tagName !== 'SPAN') {
|
|
151
|
+
var span = document.createElement('span');
|
|
152
|
+
span.className = 'pagination__link pagination__link--current';
|
|
153
|
+
span.setAttribute('aria-current', 'page');
|
|
154
|
+
span.setAttribute('data-page', page);
|
|
155
|
+
span.textContent = el.textContent;
|
|
156
|
+
parent.replaceChild(span, el);
|
|
157
|
+
} else if (!isCurrent && el.tagName !== 'A') {
|
|
158
|
+
var a = document.createElement('a');
|
|
159
|
+
a.className = 'pagination__link';
|
|
160
|
+
a.href = getHref(nav, num);
|
|
161
|
+
a.setAttribute('aria-label', 'Page ' + page);
|
|
162
|
+
a.setAttribute('data-page', page);
|
|
163
|
+
a.textContent = el.textContent;
|
|
164
|
+
parent.replaceChild(a, el);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
['first', 'prev', 'next', 'last'].forEach(function(role) {
|
|
169
|
+
var el = getRoleElement(nav, role);
|
|
170
|
+
if (!el) return;
|
|
171
|
+
var disabled = (role === 'first' || role === 'prev') ? pageNum <= 1 : pageNum >= total;
|
|
172
|
+
var hrefPage = role === 'first' ? 1 : role === 'last' ? total : role === 'prev' ? pageNum - 1 : pageNum + 1;
|
|
173
|
+
var parent = el.parentNode;
|
|
174
|
+
if (disabled) {
|
|
175
|
+
if (el.tagName !== 'SPAN') {
|
|
176
|
+
var span = document.createElement('span');
|
|
177
|
+
span.className = 'pagination__link pagination__link--' + (role === 'first' || role === 'prev' ? 'prev' : 'next') + ' pagination__link--disabled';
|
|
178
|
+
span.setAttribute('aria-disabled', 'true');
|
|
179
|
+
span.setAttribute('data-pagination-role', role);
|
|
180
|
+
span.textContent = el.textContent;
|
|
181
|
+
parent.replaceChild(span, el);
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
if (el.tagName !== 'A') {
|
|
185
|
+
var a = document.createElement('a');
|
|
186
|
+
a.className = 'pagination__link pagination__link--' + (role === 'first' || role === 'prev' ? 'prev' : 'next');
|
|
187
|
+
a.href = getHref(nav, hrefPage);
|
|
188
|
+
a.setAttribute('aria-label', role === 'first' ? 'First page' : role === 'last' ? 'Last page' : role === 'prev' ? 'Previous page' : 'Next page');
|
|
189
|
+
a.setAttribute('data-pagination-role', role);
|
|
190
|
+
a.textContent = el.textContent;
|
|
191
|
+
parent.replaceChild(a, el);
|
|
192
|
+
} else {
|
|
193
|
+
el.href = getHref(nav, hrefPage);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function init() {
|
|
199
|
+
document.querySelectorAll('[data-pagination-sync="true"]').forEach(function(nav) {
|
|
200
|
+
var total = parseInt(nav.getAttribute('data-total-pages'), 10) || 1;
|
|
201
|
+
var hash = window.location.hash;
|
|
202
|
+
var m = hash && hash.match(/^#page-(\d+)$/);
|
|
203
|
+
var page = m ? parseInt(m[1], 10) : null;
|
|
204
|
+
if (page >= 1 && page <= total) setCurrentPage(nav, page);
|
|
205
|
+
|
|
206
|
+
if (nav.hasAttribute('data-pagination-initialized')) return;
|
|
207
|
+
nav.setAttribute('data-pagination-initialized', 'true');
|
|
208
|
+
nav.addEventListener('click', function(e) {
|
|
209
|
+
var a = e.target.closest('a.pagination__link');
|
|
210
|
+
if (!a || !a.href) return;
|
|
211
|
+
var href = a.getAttribute('href') || a.href || '';
|
|
212
|
+
var match = href.match(/#page-(\d+)/);
|
|
213
|
+
if (!match) return;
|
|
214
|
+
e.preventDefault();
|
|
215
|
+
var pageNum = parseInt(match[1], 10);
|
|
216
|
+
if (pageNum >= 1 && pageNum <= total) {
|
|
217
|
+
window.location.hash = '#page-' + pageNum;
|
|
218
|
+
setCurrentPage(nav, pageNum);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (window.__paginationSyncHashListener) return;
|
|
224
|
+
window.__paginationSyncHashListener = true;
|
|
225
|
+
window.addEventListener('hashchange', function() {
|
|
226
|
+
var hash = window.location.hash;
|
|
227
|
+
var m = hash && hash.match(/^#page-(\d+)$/);
|
|
228
|
+
if (!m) return;
|
|
229
|
+
var pageNum = parseInt(m[1], 10);
|
|
230
|
+
document.querySelectorAll('[data-pagination-sync="true"]').forEach(function(nav) {
|
|
231
|
+
var total = parseInt(nav.getAttribute('data-total-pages'), 10) || 1;
|
|
232
|
+
if (pageNum >= 1 && pageNum <= total) setCurrentPage(nav, pageNum);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
|
237
|
+
else init();
|
|
238
|
+
})();
|
|
239
|
+
</script>
|
|
240
|
+
)}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Current value (0 to max) */
|
|
4
|
+
value?: number;
|
|
5
|
+
/** Maximum value (default: 100) */
|
|
6
|
+
max?: number;
|
|
7
|
+
/** Visual variant */
|
|
8
|
+
variant?: 'primary' | 'success' | 'warning' | 'error' | 'info';
|
|
9
|
+
/** Bar height size */
|
|
10
|
+
size?: 'sm' | 'md' | 'lg';
|
|
11
|
+
/** Show percentage or value label */
|
|
12
|
+
showLabel?: boolean;
|
|
13
|
+
/** Indeterminate (animated) state - ignores value */
|
|
14
|
+
indeterminate?: boolean;
|
|
15
|
+
/** Accessible label for the progress bar */
|
|
16
|
+
label?: string;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
value = 0,
|
|
22
|
+
max = 100,
|
|
23
|
+
variant = 'primary',
|
|
24
|
+
size = 'md',
|
|
25
|
+
showLabel = false,
|
|
26
|
+
indeterminate = false,
|
|
27
|
+
label,
|
|
28
|
+
class: className = '',
|
|
29
|
+
} = Astro.props;
|
|
30
|
+
|
|
31
|
+
const safeMax = max <= 0 ? 100 : max;
|
|
32
|
+
const clampedValue = indeterminate ? 0 : Math.max(0, Math.min(value, safeMax));
|
|
33
|
+
const percentage = indeterminate ? 0 : Math.round((clampedValue / safeMax) * 100);
|
|
34
|
+
|
|
35
|
+
const variantClass = `progress--${variant}`;
|
|
36
|
+
const sizeClass = `progress--${size}`;
|
|
37
|
+
const indeterminateClass = indeterminate ? 'progress--indeterminate' : '';
|
|
38
|
+
const classes = `progress ${variantClass} ${sizeClass} ${indeterminateClass} ${className}`.trim();
|
|
39
|
+
|
|
40
|
+
const ariaAttrs = indeterminate
|
|
41
|
+
? { 'aria-valuemin': 0, 'aria-valuemax': safeMax, 'aria-label': label ?? 'Loading', 'aria-valuetext': 'Loading' }
|
|
42
|
+
: {
|
|
43
|
+
'aria-valuenow': clampedValue,
|
|
44
|
+
'aria-valuemin': 0,
|
|
45
|
+
'aria-valuemax': safeMax,
|
|
46
|
+
'aria-label': label ?? undefined,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const barStyle = indeterminate ? {} : { width: `${percentage}%` };
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<div
|
|
53
|
+
class={classes}
|
|
54
|
+
role="progressbar"
|
|
55
|
+
{...ariaAttrs}
|
|
56
|
+
>
|
|
57
|
+
<div class="progress__track">
|
|
58
|
+
<div class="progress__bar" style={barStyle} />
|
|
59
|
+
</div>
|
|
60
|
+
{showLabel && !indeterminate && (
|
|
61
|
+
<span class="progress__label" aria-hidden="true">
|
|
62
|
+
{percentage}%
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
id?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
value?: string;
|
|
6
|
+
checked?: boolean;
|
|
7
|
+
required?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
class?: string;
|
|
10
|
+
ariaDescribedby?: string;
|
|
11
|
+
ariaLabel?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const {
|
|
15
|
+
id,
|
|
16
|
+
name,
|
|
17
|
+
value,
|
|
18
|
+
checked = false,
|
|
19
|
+
required = false,
|
|
20
|
+
disabled = false,
|
|
21
|
+
class: className = '',
|
|
22
|
+
ariaDescribedby,
|
|
23
|
+
ariaLabel,
|
|
24
|
+
} = Astro.props;
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
<input
|
|
28
|
+
type="radio"
|
|
29
|
+
id={id}
|
|
30
|
+
name={name}
|
|
31
|
+
value={value}
|
|
32
|
+
checked={checked}
|
|
33
|
+
required={required}
|
|
34
|
+
disabled={disabled}
|
|
35
|
+
class={className}
|
|
36
|
+
aria-describedby={ariaDescribedby}
|
|
37
|
+
aria-label={ariaLabel}
|
|
38
|
+
/>
|