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.
Files changed (93) hide show
  1. package/README.md +13 -7
  2. package/bin/rizzo-css.js +303 -0
  3. package/dist/rizzo.min.css +1 -1
  4. package/package.json +13 -4
  5. package/scaffold/astro/Accordion.astro +178 -0
  6. package/scaffold/astro/Alert.astro +131 -0
  7. package/scaffold/astro/Avatar.astro +59 -0
  8. package/scaffold/astro/Badge.astro +24 -0
  9. package/scaffold/astro/Breadcrumb.astro +61 -0
  10. package/scaffold/astro/Button.astro +3 -0
  11. package/scaffold/astro/Card.astro +18 -0
  12. package/scaffold/astro/Checkbox.astro +38 -0
  13. package/scaffold/astro/CopyToClipboard.astro +199 -0
  14. package/scaffold/astro/Divider.astro +37 -0
  15. package/scaffold/astro/Dropdown.astro +807 -0
  16. package/scaffold/astro/FormGroup.astro +51 -0
  17. package/scaffold/astro/Input.astro +59 -0
  18. package/scaffold/astro/Modal.astro +212 -0
  19. package/scaffold/astro/Pagination.astro +240 -0
  20. package/scaffold/astro/ProgressBar.astro +65 -0
  21. package/scaffold/astro/Radio.astro +38 -0
  22. package/scaffold/astro/Select.astro +49 -0
  23. package/scaffold/astro/Spinner.astro +30 -0
  24. package/scaffold/astro/Table.astro +181 -0
  25. package/scaffold/astro/Tabs.astro +223 -0
  26. package/scaffold/astro/Textarea.astro +58 -0
  27. package/scaffold/astro/Toast.astro +30 -0
  28. package/scaffold/astro/Tooltip.astro +32 -0
  29. package/scaffold/astro/icons/Brush.astro +11 -0
  30. package/scaffold/astro/icons/Cake.astro +12 -0
  31. package/scaffold/astro/icons/Check.astro +30 -0
  32. package/scaffold/astro/icons/Cherry.astro +12 -0
  33. package/scaffold/astro/icons/ChevronDown.astro +30 -0
  34. package/scaffold/astro/icons/Circle.astro +30 -0
  35. package/scaffold/astro/icons/Close.astro +31 -0
  36. package/scaffold/astro/icons/Copy.astro +31 -0
  37. package/scaffold/astro/icons/Eye.astro +31 -0
  38. package/scaffold/astro/icons/Filter.astro +30 -0
  39. package/scaffold/astro/icons/Flame.astro +29 -0
  40. package/scaffold/astro/icons/Flower.astro +12 -0
  41. package/scaffold/astro/icons/Gear.astro +31 -0
  42. package/scaffold/astro/icons/Heart.astro +29 -0
  43. package/scaffold/astro/icons/IceCream.astro +32 -0
  44. package/scaffold/astro/icons/Leaf.astro +30 -0
  45. package/scaffold/astro/icons/Lemon.astro +12 -0
  46. package/scaffold/astro/icons/Moon.astro +30 -0
  47. package/scaffold/astro/icons/Owl.astro +35 -0
  48. package/scaffold/astro/icons/Palette.astro +34 -0
  49. package/scaffold/astro/icons/Rainbow.astro +32 -0
  50. package/scaffold/astro/icons/Search.astro +31 -0
  51. package/scaffold/astro/icons/Shield.astro +29 -0
  52. package/scaffold/astro/icons/Snowflake.astro +35 -0
  53. package/scaffold/astro/icons/Sort.astro +31 -0
  54. package/scaffold/astro/icons/Sun.astro +30 -0
  55. package/scaffold/astro/icons/Sunset.astro +11 -0
  56. package/scaffold/astro/icons/Zap.astro +10 -0
  57. package/scaffold/astro/icons/devicons/Astro.astro +54 -0
  58. package/scaffold/astro/icons/devicons/Bash.astro +35 -0
  59. package/scaffold/astro/icons/devicons/Css3.astro +30 -0
  60. package/scaffold/astro/icons/devicons/Git.astro +25 -0
  61. package/scaffold/astro/icons/devicons/Html5.astro +28 -0
  62. package/scaffold/astro/icons/devicons/Javascript.astro +26 -0
  63. package/scaffold/astro/icons/devicons/Nodejs.astro +48 -0
  64. package/scaffold/astro/icons/devicons/Plaintext.astro +34 -0
  65. package/scaffold/astro/icons/devicons/React.astro +28 -0
  66. package/scaffold/astro/icons/devicons/Svelte.astro +26 -0
  67. package/scaffold/astro/icons/devicons/Vue.astro +27 -0
  68. package/scaffold/svelte/.gitkeep +0 -0
  69. package/scaffold/svelte/Accordion.svelte +128 -0
  70. package/scaffold/svelte/Alert.svelte +79 -0
  71. package/scaffold/svelte/Avatar.svelte +39 -0
  72. package/scaffold/svelte/Badge.svelte +31 -0
  73. package/scaffold/svelte/Breadcrumb.svelte +46 -0
  74. package/scaffold/svelte/Button.svelte +23 -0
  75. package/scaffold/svelte/Card.svelte +14 -0
  76. package/scaffold/svelte/Checkbox.svelte +37 -0
  77. package/scaffold/svelte/CopyToClipboard.svelte +76 -0
  78. package/scaffold/svelte/Divider.svelte +28 -0
  79. package/scaffold/svelte/Dropdown.svelte +237 -0
  80. package/scaffold/svelte/FormGroup.svelte +41 -0
  81. package/scaffold/svelte/Input.svelte +57 -0
  82. package/scaffold/svelte/Modal.svelte +152 -0
  83. package/scaffold/svelte/Pagination.svelte +93 -0
  84. package/scaffold/svelte/ProgressBar.svelte +56 -0
  85. package/scaffold/svelte/Radio.svelte +38 -0
  86. package/scaffold/svelte/Select.svelte +47 -0
  87. package/scaffold/svelte/Spinner.svelte +14 -0
  88. package/scaffold/svelte/Table.svelte +155 -0
  89. package/scaffold/svelte/Tabs.svelte +109 -0
  90. package/scaffold/svelte/Textarea.svelte +57 -0
  91. package/scaffold/svelte/Toast.svelte +30 -0
  92. package/scaffold/svelte/Tooltip.svelte +19 -0
  93. 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
+ />