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,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Close from './icons/Close.astro';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: 'success' | 'error' | 'warning' | 'info';
|
|
6
|
+
dismissible?: boolean;
|
|
7
|
+
autoDismiss?: number; // Duration in milliseconds (0 = disabled)
|
|
8
|
+
class?: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const {
|
|
13
|
+
variant = 'info',
|
|
14
|
+
dismissible = false,
|
|
15
|
+
autoDismiss = 0, // 0 means disabled
|
|
16
|
+
class: className = '',
|
|
17
|
+
id,
|
|
18
|
+
} = Astro.props;
|
|
19
|
+
|
|
20
|
+
const alertId = id || `alert-${Math.random().toString(36).substr(2, 9)}`;
|
|
21
|
+
const variantClass = `alert--${variant}`;
|
|
22
|
+
const classes = `alert ${variantClass} ${className}`.trim();
|
|
23
|
+
|
|
24
|
+
// ARIA labels based on variant
|
|
25
|
+
const ariaLabels = {
|
|
26
|
+
success: 'Success message',
|
|
27
|
+
error: 'Error message',
|
|
28
|
+
warning: 'Warning message',
|
|
29
|
+
info: 'Information message',
|
|
30
|
+
};
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
<div
|
|
34
|
+
class={classes}
|
|
35
|
+
id={alertId}
|
|
36
|
+
role="alert"
|
|
37
|
+
aria-live="polite"
|
|
38
|
+
aria-atomic="true"
|
|
39
|
+
aria-label={ariaLabels[variant]}
|
|
40
|
+
>
|
|
41
|
+
<div class="alert__content">
|
|
42
|
+
<slot />
|
|
43
|
+
</div>
|
|
44
|
+
{dismissible && (
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class="alert__close"
|
|
48
|
+
aria-label="Dismiss alert"
|
|
49
|
+
data-alert-close
|
|
50
|
+
aria-controls={alertId}
|
|
51
|
+
>
|
|
52
|
+
<Close width={16} height={16} />
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<script define:vars={{ alertId, autoDismiss }}>
|
|
58
|
+
(function initAlert() {
|
|
59
|
+
const init = () => {
|
|
60
|
+
const alert = document.querySelector(`#${alertId}`);
|
|
61
|
+
if (!alert) {
|
|
62
|
+
if (document.readyState === 'loading') {
|
|
63
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Skip if already initialized
|
|
70
|
+
if (alert.hasAttribute('data-alert-initialized')) return;
|
|
71
|
+
alert.setAttribute('data-alert-initialized', 'true');
|
|
72
|
+
|
|
73
|
+
const closeBtn = alert.querySelector('[data-alert-close]');
|
|
74
|
+
|
|
75
|
+
let closeAlert = () => {
|
|
76
|
+
// Announce dismissal to screen readers
|
|
77
|
+
const announcement = document.createElement('div');
|
|
78
|
+
announcement.setAttribute('role', 'status');
|
|
79
|
+
announcement.setAttribute('aria-live', 'polite');
|
|
80
|
+
announcement.className = 'sr-only';
|
|
81
|
+
announcement.textContent = 'Alert dismissed';
|
|
82
|
+
document.body.appendChild(announcement);
|
|
83
|
+
|
|
84
|
+
// Remove alert with animation
|
|
85
|
+
alert.style.opacity = '0';
|
|
86
|
+
alert.style.transform = 'translateY(-10px)';
|
|
87
|
+
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
alert.remove();
|
|
90
|
+
if (document.body.contains(announcement)) {
|
|
91
|
+
document.body.removeChild(announcement);
|
|
92
|
+
}
|
|
93
|
+
}, 200);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Auto-dismiss functionality
|
|
97
|
+
if (autoDismiss > 0) {
|
|
98
|
+
const timeoutId = setTimeout(() => {
|
|
99
|
+
closeAlert();
|
|
100
|
+
}, autoDismiss);
|
|
101
|
+
|
|
102
|
+
// Clear timeout if user manually dismisses
|
|
103
|
+
if (closeBtn) {
|
|
104
|
+
const originalClose = closeAlert;
|
|
105
|
+
closeAlert = () => {
|
|
106
|
+
clearTimeout(timeoutId);
|
|
107
|
+
originalClose();
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (closeBtn) {
|
|
113
|
+
closeBtn.addEventListener('click', closeAlert);
|
|
114
|
+
|
|
115
|
+
// Keyboard support
|
|
116
|
+
closeBtn.addEventListener('keydown', (e) => {
|
|
117
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
closeAlert();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (document.readyState === 'loading') {
|
|
126
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
127
|
+
} else {
|
|
128
|
+
init();
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
</script>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Image } from 'astro:assets';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
/** Image URL (when provided, shows image; otherwise shows initials or placeholder). For remote URLs, add the domain to image.domains in astro.config. */
|
|
6
|
+
src?: string;
|
|
7
|
+
/** Alt text for the image */
|
|
8
|
+
alt?: string;
|
|
9
|
+
/** Full name used to derive initials when no image (e.g. "Jane Doe" → "JD") */
|
|
10
|
+
name?: string;
|
|
11
|
+
/** Override initials when no image (e.g. "JD"); ignored if name is provided */
|
|
12
|
+
initials?: string;
|
|
13
|
+
/** Size */
|
|
14
|
+
size?: 'sm' | 'md' | 'lg';
|
|
15
|
+
/** Shape */
|
|
16
|
+
shape?: 'circle' | 'square';
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const {
|
|
21
|
+
src,
|
|
22
|
+
alt = '',
|
|
23
|
+
name = '',
|
|
24
|
+
initials: initialsProp = '',
|
|
25
|
+
size = 'md',
|
|
26
|
+
shape = 'circle',
|
|
27
|
+
class: className = '',
|
|
28
|
+
} = Astro.props;
|
|
29
|
+
|
|
30
|
+
function getInitials(name: string): string {
|
|
31
|
+
const parts = name.trim().split(/\s+/).filter(Boolean);
|
|
32
|
+
if (parts.length === 0) return '';
|
|
33
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
34
|
+
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const displayInitials = name ? getInitials(name) : initialsProp;
|
|
38
|
+
const sizeClass = `avatar--${size}`;
|
|
39
|
+
const shapeClass = `avatar--${shape}`;
|
|
40
|
+
const classes = `avatar ${sizeClass} ${shapeClass} ${className}`.trim();
|
|
41
|
+
|
|
42
|
+
const sizePixels = size === 'sm' ? 32 : size === 'lg' ? 48 : 40;
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
<span class={classes} role="img" aria-label={alt || name || (displayInitials ? `Avatar: ${displayInitials}` : 'Avatar')}>
|
|
46
|
+
{src ? (
|
|
47
|
+
<Image
|
|
48
|
+
src={src}
|
|
49
|
+
alt={alt || name || ''}
|
|
50
|
+
width={sizePixels}
|
|
51
|
+
height={sizePixels}
|
|
52
|
+
class="avatar__img"
|
|
53
|
+
/>
|
|
54
|
+
) : (
|
|
55
|
+
<span class="avatar__initials" aria-hidden="true">
|
|
56
|
+
{displayInitials || '?'}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
variant?: 'primary' | 'success' | 'warning' | 'error' | 'info';
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
pill?: boolean;
|
|
6
|
+
class?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
variant = 'primary',
|
|
11
|
+
size = 'md',
|
|
12
|
+
pill = false,
|
|
13
|
+
class: className = '',
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
|
|
16
|
+
const variantClass = `badge--${variant}`;
|
|
17
|
+
const sizeClass = `badge--${size}`;
|
|
18
|
+
const pillClass = pill ? 'badge--pill' : '';
|
|
19
|
+
const classes = `badge ${variantClass} ${sizeClass} ${pillClass} ${className}`.trim();
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<span class={classes}>
|
|
23
|
+
<slot />
|
|
24
|
+
</span>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
import ChevronDown from './icons/ChevronDown.astro';
|
|
3
|
+
|
|
4
|
+
interface BreadcrumbItem {
|
|
5
|
+
label: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
items: BreadcrumbItem[];
|
|
11
|
+
separator?: 'chevron' | 'slash' | 'arrow' | string;
|
|
12
|
+
class?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const {
|
|
16
|
+
items,
|
|
17
|
+
separator = 'chevron',
|
|
18
|
+
class: className = '',
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const separatorVariant =
|
|
22
|
+
separator === 'slash' ? 'breadcrumb--slash' :
|
|
23
|
+
separator === 'arrow' ? 'breadcrumb--arrow' :
|
|
24
|
+
'breadcrumb--chevron';
|
|
25
|
+
const classes = `breadcrumb ${separatorVariant} ${className}`.trim();
|
|
26
|
+
|
|
27
|
+
const separatorChar = separator === 'slash' ? '/' : separator === 'arrow' ? '›' : typeof separator === 'string' ? separator : null;
|
|
28
|
+
const useIcon = separator === 'chevron';
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
<nav class={classes} aria-label="Breadcrumb">
|
|
32
|
+
<ol class="breadcrumb__list">
|
|
33
|
+
{items.map((item, index) => {
|
|
34
|
+
const isLast = index === items.length - 1;
|
|
35
|
+
const isCurrent = isLast || !item.href;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<li class={`breadcrumb__item ${isCurrent ? 'breadcrumb__item--current' : ''}`}>
|
|
39
|
+
{isCurrent ? (
|
|
40
|
+
<span class="breadcrumb__current" aria-current="page">
|
|
41
|
+
{item.label}
|
|
42
|
+
</span>
|
|
43
|
+
) : (
|
|
44
|
+
<a class="breadcrumb__link" href={item.href}>
|
|
45
|
+
{item.label}
|
|
46
|
+
</a>
|
|
47
|
+
)}
|
|
48
|
+
{!isLast && (
|
|
49
|
+
<span class="breadcrumb__separator" aria-hidden="true">
|
|
50
|
+
{useIcon ? (
|
|
51
|
+
<ChevronDown class="breadcrumb__separator-icon" width={14} height={14} />
|
|
52
|
+
) : (
|
|
53
|
+
separatorChar ?? '›'
|
|
54
|
+
)}
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
})}
|
|
60
|
+
</ol>
|
|
61
|
+
</nav>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
variant?: 'default' | 'elevated' | 'outlined' | 'filled';
|
|
4
|
+
class?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
variant = 'default',
|
|
9
|
+
class: className = '',
|
|
10
|
+
} = Astro.props;
|
|
11
|
+
|
|
12
|
+
const variantClass = variant !== 'default' ? `card--${variant}` : '';
|
|
13
|
+
const classes = `card ${variantClass} ${className}`.trim();
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<div class={classes}>
|
|
17
|
+
<slot />
|
|
18
|
+
</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="checkbox"
|
|
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
|
+
/>
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Copy from './icons/Copy.astro';
|
|
3
|
+
import Check from './icons/Check.astro';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
value: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
format?: string;
|
|
9
|
+
class?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const {
|
|
14
|
+
value,
|
|
15
|
+
label,
|
|
16
|
+
format,
|
|
17
|
+
class: className = '',
|
|
18
|
+
id,
|
|
19
|
+
} = Astro.props;
|
|
20
|
+
|
|
21
|
+
const buttonId = id || `copy-btn-${Math.random().toString(36).substr(2, 9)}`;
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
class={`copy-to-clipboard ${className}`.trim()}
|
|
27
|
+
data-copy-value={value}
|
|
28
|
+
data-copy-format={format}
|
|
29
|
+
aria-label={label || `Copy ${value} to clipboard`}
|
|
30
|
+
id={buttonId}
|
|
31
|
+
>
|
|
32
|
+
<span class="copy-to-clipboard__text">{value}</span>
|
|
33
|
+
<span class="copy-to-clipboard__icon copy-to-clipboard__icon--copy" aria-hidden="true">
|
|
34
|
+
<Copy width={16} height={16} />
|
|
35
|
+
</span>
|
|
36
|
+
<span class="copy-to-clipboard__icon copy-to-clipboard__icon--check" aria-hidden="true">
|
|
37
|
+
<Check width={16} height={16} />
|
|
38
|
+
</span>
|
|
39
|
+
<span class="copy-to-clipboard__feedback" aria-live="polite"></span>
|
|
40
|
+
</button>
|
|
41
|
+
|
|
42
|
+
<script>
|
|
43
|
+
(function initCopyToClipboard() {
|
|
44
|
+
// Wait for DOM to be ready
|
|
45
|
+
const init = () => {
|
|
46
|
+
const buttons = document.querySelectorAll('.copy-to-clipboard');
|
|
47
|
+
|
|
48
|
+
buttons.forEach((button) => {
|
|
49
|
+
// Skip if already initialized
|
|
50
|
+
if (button.hasAttribute('data-copy-initialized')) return;
|
|
51
|
+
button.setAttribute('data-copy-initialized', 'true');
|
|
52
|
+
|
|
53
|
+
const getValue = () => button.getAttribute('data-copy-value') || '';
|
|
54
|
+
const getFormat = () => button.getAttribute('data-copy-format') || '';
|
|
55
|
+
const feedback = button.querySelector('.copy-to-clipboard__feedback');
|
|
56
|
+
const copyIcon = button.querySelector('.copy-to-clipboard__icon--copy');
|
|
57
|
+
const checkIcon = button.querySelector('.copy-to-clipboard__icon--check');
|
|
58
|
+
const textSpan = button.querySelector('.copy-to-clipboard__text');
|
|
59
|
+
|
|
60
|
+
// Update text span with current value
|
|
61
|
+
const updateDisplay = () => {
|
|
62
|
+
const value = getValue();
|
|
63
|
+
if (textSpan) {
|
|
64
|
+
textSpan.textContent = value || '';
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Initial display update
|
|
69
|
+
updateDisplay();
|
|
70
|
+
|
|
71
|
+
// Watch for attribute changes (for dynamic updates)
|
|
72
|
+
const observer = new MutationObserver((mutations) => {
|
|
73
|
+
let shouldUpdate = false;
|
|
74
|
+
mutations.forEach((mutation) => {
|
|
75
|
+
if (mutation.type === 'attributes') {
|
|
76
|
+
if (mutation.attributeName === 'data-copy-value' || mutation.attributeName === 'data-copy-format') {
|
|
77
|
+
shouldUpdate = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
if (shouldUpdate) {
|
|
82
|
+
// Use requestAnimationFrame to ensure DOM is ready
|
|
83
|
+
requestAnimationFrame(() => {
|
|
84
|
+
updateDisplay();
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
observer.observe(button, {
|
|
89
|
+
attributes: true,
|
|
90
|
+
attributeFilter: ['data-copy-value', 'data-copy-format'],
|
|
91
|
+
attributeOldValue: false
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Listen for custom value-updated event
|
|
95
|
+
button.addEventListener('value-updated', (event) => {
|
|
96
|
+
if (event && event.detail && event.detail.value) {
|
|
97
|
+
updateDisplay();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Fallback: Periodically check for value changes (in case MutationObserver misses something)
|
|
102
|
+
let lastValue = getValue();
|
|
103
|
+
const intervalId = setInterval(() => {
|
|
104
|
+
const currentValue = getValue();
|
|
105
|
+
if (currentValue !== lastValue) {
|
|
106
|
+
lastValue = currentValue;
|
|
107
|
+
updateDisplay();
|
|
108
|
+
}
|
|
109
|
+
}, 300);
|
|
110
|
+
|
|
111
|
+
// Store interval ID for potential cleanup (though we don't need to clean it up in this case)
|
|
112
|
+
button.setAttribute('data-copy-interval-id', intervalId.toString());
|
|
113
|
+
|
|
114
|
+
const copyToClipboard = async () => {
|
|
115
|
+
// Always read fresh value on click (in case it was updated dynamically)
|
|
116
|
+
let value = getValue();
|
|
117
|
+
const format = getFormat();
|
|
118
|
+
|
|
119
|
+
// If value is still empty, try reading from text span as fallback
|
|
120
|
+
if (!value && textSpan) {
|
|
121
|
+
value = textSpan.textContent || '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!value) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await navigator.clipboard.writeText(value);
|
|
130
|
+
|
|
131
|
+
// Show success state
|
|
132
|
+
if (copyIcon) copyIcon.classList.add('copy-to-clipboard__icon--hidden');
|
|
133
|
+
if (checkIcon) checkIcon.classList.remove('copy-to-clipboard__icon--hidden');
|
|
134
|
+
if (feedback) {
|
|
135
|
+
feedback.textContent = format ? `Copied ${format}!` : 'Copied!';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Reset after 2 seconds
|
|
139
|
+
setTimeout(() => {
|
|
140
|
+
if (copyIcon) copyIcon.classList.remove('copy-to-clipboard__icon--hidden');
|
|
141
|
+
if (checkIcon) checkIcon.classList.add('copy-to-clipboard__icon--hidden');
|
|
142
|
+
if (feedback) {
|
|
143
|
+
feedback.textContent = '';
|
|
144
|
+
}
|
|
145
|
+
}, 2000);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
// Fallback for older browsers
|
|
148
|
+
const textArea = document.createElement('textarea');
|
|
149
|
+
textArea.value = value;
|
|
150
|
+
textArea.style.position = 'fixed';
|
|
151
|
+
textArea.style.left = '-999999px';
|
|
152
|
+
document.body.appendChild(textArea);
|
|
153
|
+
textArea.focus();
|
|
154
|
+
textArea.select();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
document.execCommand('copy');
|
|
158
|
+
if (copyIcon) copyIcon.classList.add('copy-to-clipboard__icon--hidden');
|
|
159
|
+
if (checkIcon) checkIcon.classList.remove('copy-to-clipboard__icon--hidden');
|
|
160
|
+
if (feedback) {
|
|
161
|
+
feedback.textContent = format ? `Copied ${format}!` : 'Copied!';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setTimeout(() => {
|
|
165
|
+
if (copyIcon) copyIcon.classList.remove('copy-to-clipboard__icon--hidden');
|
|
166
|
+
if (checkIcon) checkIcon.classList.add('copy-to-clipboard__icon--hidden');
|
|
167
|
+
if (feedback) {
|
|
168
|
+
feedback.textContent = '';
|
|
169
|
+
}
|
|
170
|
+
}, 2000);
|
|
171
|
+
} catch (fallbackErr) {
|
|
172
|
+
if (feedback) {
|
|
173
|
+
feedback.textContent = 'Failed to copy';
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
document.body.removeChild(textArea);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
button.addEventListener('click', copyToClipboard);
|
|
182
|
+
|
|
183
|
+
// Keyboard support
|
|
184
|
+
button.addEventListener('keydown', (e) => {
|
|
185
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
copyToClipboard();
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
if (document.readyState === 'loading') {
|
|
194
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
195
|
+
} else {
|
|
196
|
+
init();
|
|
197
|
+
}
|
|
198
|
+
})();
|
|
199
|
+
</script>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Orientation of the divider line */
|
|
4
|
+
orientation?: 'horizontal' | 'vertical';
|
|
5
|
+
/** Optional label (e.g. "OR") shown in the middle; only used for horizontal */
|
|
6
|
+
label?: string;
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
orientation = 'horizontal',
|
|
12
|
+
label,
|
|
13
|
+
class: className = '',
|
|
14
|
+
} = Astro.props;
|
|
15
|
+
|
|
16
|
+
const orientationClass = `divider--${orientation}`;
|
|
17
|
+
const hasLabel = Boolean(label && label.trim());
|
|
18
|
+
const labelClass = hasLabel ? 'divider--labeled' : '';
|
|
19
|
+
const classes = `divider ${orientationClass} ${labelClass} ${className}`.trim();
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div
|
|
23
|
+
class={classes}
|
|
24
|
+
role="separator"
|
|
25
|
+
aria-orientation={orientation}
|
|
26
|
+
aria-label={label?.trim() || undefined}
|
|
27
|
+
>
|
|
28
|
+
{hasLabel && orientation === 'horizontal' ? (
|
|
29
|
+
<>
|
|
30
|
+
<span class="divider__line" aria-hidden="true" />
|
|
31
|
+
<span class="divider__label">{label!.trim()}</span>
|
|
32
|
+
<span class="divider__line" aria-hidden="true" />
|
|
33
|
+
</>
|
|
34
|
+
) : (
|
|
35
|
+
<span class="divider__line" aria-hidden="true" />
|
|
36
|
+
)}
|
|
37
|
+
</div>
|