zabi-components 5.0.11 → 5.0.13
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/dist/atoms/Badge.svelte +18 -15
- package/dist/atoms/Button.svelte +41 -13
- package/dist/atoms/Card.svelte +31 -7
- package/dist/atoms/Card.svelte.d.ts +1 -0
- package/dist/atoms/Checkbox.svelte +64 -22
- package/dist/atoms/Input.svelte +61 -10
- package/dist/atoms/Input.svelte.d.ts +1 -0
- package/dist/atoms/Select.svelte +165 -34
- package/dist/atoms/Select.svelte.d.ts +2 -0
- package/dist/atoms/ThemeToggle.svelte +75 -12
- package/dist/atoms/ThemeToggle.svelte.d.ts +2 -1
- package/dist/atoms/Toggle.svelte +5 -5
- package/dist/molecules/Dropdown.svelte +44 -106
- package/dist/molecules/ImageUpload.svelte +15 -5
- package/dist/molecules/Modal.svelte +23 -19
- package/dist/molecules/SlideUp.svelte +37 -31
- package/dist/organisms/Navigation.svelte +80 -18
- package/dist/organisms/Navigation.svelte.d.ts +9 -5
- package/dist/zabi-components.css +166 -0
- package/package.json +1 -1
package/dist/atoms/Select.svelte
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import Dropdown from "../molecules/Dropdown.svelte";
|
|
3
|
+
import { ChevronDown, CheckCircle, AlertTriangle, AlertCircle } from "@lucide/svelte";
|
|
4
|
+
|
|
2
5
|
// SSR-safe ID generation
|
|
3
6
|
function generateId(prefix: string = "id"): string {
|
|
4
7
|
if (typeof window !== "undefined") {
|
|
@@ -19,6 +22,8 @@
|
|
|
19
22
|
label?: string;
|
|
20
23
|
disabled?: boolean;
|
|
21
24
|
size?: "sm" | "md" | "lg";
|
|
25
|
+
variant?: "default" | "success" | "warning" | "error";
|
|
26
|
+
message?: string;
|
|
22
27
|
onchange?: (event: Event) => void;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -29,27 +34,55 @@
|
|
|
29
34
|
label = "",
|
|
30
35
|
disabled = false,
|
|
31
36
|
size = "md",
|
|
37
|
+
variant = "default",
|
|
38
|
+
message = "",
|
|
32
39
|
...restProps
|
|
33
40
|
}: Props = $props();
|
|
34
41
|
|
|
35
|
-
|
|
36
|
-
const selectId = generateId("select");
|
|
42
|
+
let isOpen = $state(false);
|
|
37
43
|
|
|
38
|
-
// Size classes
|
|
44
|
+
// Size classes matching M3 design specifications
|
|
39
45
|
const sizeClass = $derived(() => {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
if (size === "sm") {
|
|
47
|
+
return {
|
|
48
|
+
padding: "px-4 py-2",
|
|
49
|
+
text: "text-sm",
|
|
50
|
+
leading: "leading-5"
|
|
51
|
+
};
|
|
52
|
+
} else if (size === "lg") {
|
|
53
|
+
return {
|
|
54
|
+
padding: "px-4 py-3",
|
|
55
|
+
text: "text-base",
|
|
56
|
+
leading: "leading-6"
|
|
57
|
+
};
|
|
58
|
+
} else {
|
|
59
|
+
// default md
|
|
60
|
+
return {
|
|
61
|
+
padding: "px-4 py-2.5",
|
|
62
|
+
text: "text-base",
|
|
63
|
+
leading: "leading-6"
|
|
64
|
+
};
|
|
65
|
+
}
|
|
45
66
|
});
|
|
46
67
|
|
|
47
|
-
//
|
|
48
|
-
const
|
|
68
|
+
// Variant classes using semantic colors
|
|
69
|
+
const variantClass = $derived(() => {
|
|
70
|
+
return variant === "success"
|
|
71
|
+
? "border-success focus:border-success focus:ring-success"
|
|
72
|
+
: variant === "warning"
|
|
73
|
+
? "border-warning focus:border-warning focus:ring-warning"
|
|
74
|
+
: variant === "error"
|
|
75
|
+
? "border-error focus:border-error focus:ring-error"
|
|
76
|
+
: "border-0 focus:ring-2 focus:ring-brand-500"; // default - no border
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Trigger button classes matching M3 design
|
|
80
|
+
const triggerClasses = $derived(() => {
|
|
81
|
+
const sizeStyles = sizeClass();
|
|
49
82
|
const baseClasses =
|
|
50
|
-
"w-full
|
|
83
|
+
"w-full bg-brand-100 rounded-lg transition-all duration-200 text-body focus:outline-none focus:ring-offset-0 disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-between";
|
|
51
84
|
|
|
52
|
-
return `${baseClasses} ${
|
|
85
|
+
return `${baseClasses} ${sizeStyles.padding} ${sizeStyles.text} ${sizeStyles.leading} ${variantClass()}`.trim();
|
|
53
86
|
});
|
|
54
87
|
|
|
55
88
|
// Label classes using semantic text colors
|
|
@@ -57,32 +90,130 @@
|
|
|
57
90
|
() => "block text-sm font-medium text-label mb-1",
|
|
58
91
|
);
|
|
59
92
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
93
|
+
// Message classes based on variant
|
|
94
|
+
const messageClasses = $derived(() => {
|
|
95
|
+
if (variant === "error") {
|
|
96
|
+
return "text-error text-sm mt-1 flex items-center gap-1.5";
|
|
97
|
+
} else if (variant === "success") {
|
|
98
|
+
return "text-success text-sm mt-1 flex items-center gap-1.5";
|
|
99
|
+
} else if (variant === "warning") {
|
|
100
|
+
return "text-warning text-sm mt-1 flex items-center gap-1.5";
|
|
101
|
+
}
|
|
102
|
+
return "text-description text-sm mt-1 flex items-center gap-1.5";
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Get icon component based on variant
|
|
106
|
+
const getIcon = $derived(() => {
|
|
107
|
+
if (variant === "error") return AlertCircle;
|
|
108
|
+
if (variant === "success") return CheckCircle;
|
|
109
|
+
if (variant === "warning") return AlertTriangle;
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Generate unique ID - SSR safe
|
|
114
|
+
const selectId = generateId("select");
|
|
115
|
+
|
|
116
|
+
// Get selected option label
|
|
117
|
+
const selectedLabel = $derived(() => {
|
|
118
|
+
if (isEmpty()) {
|
|
119
|
+
return String(placeholder || "Select an option");
|
|
120
|
+
}
|
|
121
|
+
const selected = options.find((opt) => opt.value === value);
|
|
122
|
+
return selected?.label ? String(selected.label) : String(placeholder || "Select an option");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Check if value is empty
|
|
126
|
+
const isEmpty = $derived(() => {
|
|
127
|
+
return value === undefined || value === null || value === "";
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
function handleOptionClick(optionValue: string | number) {
|
|
131
|
+
if (disabled) return;
|
|
132
|
+
value = optionValue;
|
|
133
|
+
isOpen = false;
|
|
134
|
+
|
|
135
|
+
// Create a synthetic event for onchange
|
|
136
|
+
if (onchange) {
|
|
137
|
+
const syntheticEvent = new Event("change", { bubbles: true });
|
|
138
|
+
Object.defineProperty(syntheticEvent, "target", {
|
|
139
|
+
value: { value: optionValue },
|
|
140
|
+
enumerable: true
|
|
141
|
+
});
|
|
142
|
+
(onchange as (event: Event) => void)(syntheticEvent);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleTriggerClick(event: MouseEvent) {
|
|
147
|
+
if (disabled) return;
|
|
148
|
+
event.stopPropagation();
|
|
149
|
+
isOpen = !isOpen;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Close dropdown when clicking outside
|
|
153
|
+
function handleClickOutside(event: MouseEvent) {
|
|
154
|
+
if (isOpen && !(event.target as HTMLElement).closest('.select-container')) {
|
|
155
|
+
isOpen = false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Handle escape key
|
|
160
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
161
|
+
if (event.key === "Escape" && isOpen) {
|
|
162
|
+
isOpen = false;
|
|
163
|
+
}
|
|
63
164
|
}
|
|
64
165
|
</script>
|
|
65
166
|
|
|
66
|
-
<
|
|
167
|
+
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
|
|
168
|
+
|
|
169
|
+
<div class="w-full select-container">
|
|
67
170
|
{#if label}
|
|
68
171
|
<label for={selectId} class={labelClasses()}>{label}</label>
|
|
69
172
|
{/if}
|
|
70
173
|
|
|
71
|
-
<
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
174
|
+
<Dropdown isOpen={isOpen} placement="bottom-start">
|
|
175
|
+
{#snippet trigger()}
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
id={selectId}
|
|
179
|
+
class={triggerClasses()}
|
|
180
|
+
{disabled}
|
|
181
|
+
onclick={handleTriggerClick}
|
|
182
|
+
aria-haspopup="listbox"
|
|
183
|
+
aria-expanded={isOpen}
|
|
184
|
+
aria-describedby={message ? `${selectId}-message` : undefined}
|
|
185
|
+
>
|
|
186
|
+
<span class="text-left flex-1 {isEmpty() ? 'text-description' : 'text-body'}">
|
|
187
|
+
{isEmpty() ? placeholder : (options.find((opt) => opt.value === value)?.label || placeholder)}
|
|
188
|
+
</span>
|
|
189
|
+
<ChevronDown
|
|
190
|
+
size={20}
|
|
191
|
+
class="text-description transition-transform duration-200 {isOpen ? 'rotate-180' : ''}"
|
|
192
|
+
/>
|
|
193
|
+
</button>
|
|
194
|
+
{/snippet}
|
|
195
|
+
{#snippet children()}
|
|
196
|
+
<div class="px-2 py-2">
|
|
197
|
+
{#each options as option (option.value)}
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
class="w-full text-left px-4 py-2 text-body hover:bg-brand-50 transition-colors rounded-md my-0.5 {option.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'} {value === option.value ? 'bg-brand-50' : ''}"
|
|
201
|
+
onclick={() => handleOptionClick(option.value)}
|
|
202
|
+
disabled={option.disabled}
|
|
203
|
+
>
|
|
204
|
+
{option.label}
|
|
205
|
+
</button>
|
|
206
|
+
{/each}
|
|
207
|
+
</div>
|
|
208
|
+
{/snippet}
|
|
209
|
+
</Dropdown>
|
|
210
|
+
{#if message && variant !== "default"}
|
|
211
|
+
<p id={`${selectId}-message`} class={messageClasses()} role="alert">
|
|
212
|
+
{#if getIcon()}
|
|
213
|
+
{@const Icon = getIcon()}
|
|
214
|
+
<Icon size={14} class="shrink-0" />
|
|
215
|
+
{/if}
|
|
216
|
+
<span>{message}</span>
|
|
217
|
+
</p>
|
|
218
|
+
{/if}
|
|
219
|
+
</div>
|
|
@@ -9,6 +9,8 @@ interface Props {
|
|
|
9
9
|
label?: string;
|
|
10
10
|
disabled?: boolean;
|
|
11
11
|
size?: "sm" | "md" | "lg";
|
|
12
|
+
variant?: "default" | "success" | "warning" | "error";
|
|
13
|
+
message?: string;
|
|
12
14
|
onchange?: (event: Event) => void;
|
|
13
15
|
}
|
|
14
16
|
declare const Select: import("svelte").Component<Props, {}, "">;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount } from "svelte";
|
|
3
3
|
import { Sun, Moon } from "@lucide/svelte";
|
|
4
|
+
|
|
4
5
|
// SSR-safe utilities
|
|
5
6
|
function safeLocalStorage(): Storage | undefined {
|
|
6
7
|
return typeof window !== "undefined" ? localStorage : undefined;
|
|
@@ -11,12 +12,18 @@
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface Props {
|
|
14
|
-
|
|
15
|
+
size?: "sm" | "md" | "lg";
|
|
16
|
+
variant?: "default" | "ghost" | "outline";
|
|
15
17
|
onclick?: (event: Event) => void;
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
let {
|
|
20
|
+
let {
|
|
21
|
+
size = "md",
|
|
22
|
+
variant = "default",
|
|
23
|
+
...restProps
|
|
24
|
+
}: Props = $props();
|
|
19
25
|
|
|
26
|
+
let isDark = $state(false);
|
|
20
27
|
let mounted = $state(false);
|
|
21
28
|
|
|
22
29
|
onMount(() => {
|
|
@@ -27,14 +34,10 @@
|
|
|
27
34
|
const savedTheme = storage.getItem("theme");
|
|
28
35
|
let prefersDark = false;
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
if (mounted && typeof window !== "undefined" && window.matchMedia) {
|
|
37
|
+
if (window.matchMedia) {
|
|
32
38
|
try {
|
|
33
|
-
prefersDark = window.matchMedia(
|
|
34
|
-
"(prefers-color-scheme: dark)",
|
|
35
|
-
).matches;
|
|
39
|
+
prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
36
40
|
} catch (e) {
|
|
37
|
-
// Fallback if matchMedia fails
|
|
38
41
|
prefersDark = false;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
@@ -51,6 +54,10 @@
|
|
|
51
54
|
if (mounted && storage) {
|
|
52
55
|
storage.setItem("theme", isDark ? "dark" : "light");
|
|
53
56
|
}
|
|
57
|
+
|
|
58
|
+
if (onclick) {
|
|
59
|
+
(onclick as (event: Event) => void)(event);
|
|
60
|
+
}
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
function updateTheme() {
|
|
@@ -63,26 +70,82 @@
|
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
72
|
}
|
|
73
|
+
|
|
74
|
+
// Size classes matching M3 design
|
|
75
|
+
const sizeClass = $derived(() => {
|
|
76
|
+
if (size === "sm") {
|
|
77
|
+
return {
|
|
78
|
+
button: "w-8 h-8",
|
|
79
|
+
icon: 16
|
|
80
|
+
};
|
|
81
|
+
} else if (size === "lg") {
|
|
82
|
+
return {
|
|
83
|
+
button: "w-12 h-12",
|
|
84
|
+
icon: 24
|
|
85
|
+
};
|
|
86
|
+
} else {
|
|
87
|
+
// default md
|
|
88
|
+
return {
|
|
89
|
+
button: "w-10 h-10",
|
|
90
|
+
icon: 20
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Variant classes using semantic colors
|
|
96
|
+
const variantClass = $derived(() => {
|
|
97
|
+
if (variant === "ghost") {
|
|
98
|
+
return "bg-transparent hover:bg-surface-hover border-0";
|
|
99
|
+
} else if (variant === "outline") {
|
|
100
|
+
return "bg-surface-elevated hover:bg-surface-hover border border-border";
|
|
101
|
+
} else {
|
|
102
|
+
// default
|
|
103
|
+
return "bg-surface-elevated hover:bg-surface-hover border-0";
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const buttonClasses = $derived(() => {
|
|
108
|
+
const sizeStyles = sizeClass();
|
|
109
|
+
return `
|
|
110
|
+
${sizeStyles.button}
|
|
111
|
+
${variantClass()}
|
|
112
|
+
rounded-lg
|
|
113
|
+
flex
|
|
114
|
+
items-center
|
|
115
|
+
justify-center
|
|
116
|
+
text-label
|
|
117
|
+
cursor-pointer
|
|
118
|
+
transition-colors
|
|
119
|
+
duration-200
|
|
120
|
+
focus:outline-none
|
|
121
|
+
focus:ring-2
|
|
122
|
+
focus:ring-brand-500
|
|
123
|
+
focus:ring-offset-2
|
|
124
|
+
focus:ring-offset-surface
|
|
125
|
+
`.trim().replace(/\s+/g, " ");
|
|
126
|
+
});
|
|
66
127
|
</script>
|
|
67
128
|
|
|
68
129
|
{#if mounted}
|
|
69
130
|
<button
|
|
70
131
|
onclick={toggleTheme}
|
|
71
|
-
class=
|
|
132
|
+
class={buttonClasses()}
|
|
72
133
|
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
|
|
134
|
+
type="button"
|
|
73
135
|
{...restProps}
|
|
74
136
|
>
|
|
75
137
|
{#if isDark}
|
|
76
|
-
<Moon size={
|
|
138
|
+
<Moon size={sizeClass().icon} class="text-label" />
|
|
77
139
|
{:else}
|
|
78
|
-
<Sun size={
|
|
140
|
+
<Sun size={sizeClass().icon} class="text-label" />
|
|
79
141
|
{/if}
|
|
80
142
|
</button>
|
|
81
143
|
{:else}
|
|
82
144
|
<!-- SSR fallback -->
|
|
83
145
|
<button
|
|
84
|
-
class="w-10 h-10 bg-
|
|
146
|
+
class="w-10 h-10 bg-surface-elevated rounded-lg flex items-center justify-center text-label cursor-pointer"
|
|
85
147
|
aria-label="Theme toggle"
|
|
148
|
+
type="button"
|
|
86
149
|
{...restProps}
|
|
87
150
|
>
|
|
88
151
|
<Sun size={20} class="text-label" />
|
package/dist/atoms/Toggle.svelte
CHANGED
|
@@ -39,21 +39,21 @@
|
|
|
39
39
|
if (onchange) onchange({ checked });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// Toggle button classes
|
|
42
|
+
// Toggle button classes matching M3 design
|
|
43
43
|
const toggleButtonClasses = $derived(() =>
|
|
44
44
|
[
|
|
45
|
-
"relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-
|
|
46
|
-
checked ? "bg-brand-600" : "bg-
|
|
45
|
+
"relative inline-flex w-10 h-6 flex-shrink-0 cursor-pointer rounded-full border-0 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2",
|
|
46
|
+
checked ? "bg-brand-600" : "bg-stone-300",
|
|
47
47
|
disabled && "opacity-50 cursor-not-allowed",
|
|
48
48
|
]
|
|
49
49
|
.filter(Boolean)
|
|
50
50
|
.join(" "),
|
|
51
51
|
);
|
|
52
52
|
|
|
53
|
-
// Toggle thumb classes
|
|
53
|
+
// Toggle thumb classes matching M3 design
|
|
54
54
|
const toggleThumbClasses = $derived(() => {
|
|
55
55
|
const baseClasses =
|
|
56
|
-
"pointer-events-none
|
|
56
|
+
"pointer-events-none absolute top-0.5 left-0.5 w-5 h-5 rounded-full bg-white transition-transform duration-200 ease-in-out";
|
|
57
57
|
const positionClasses = checked ? "translate-x-4" : "translate-x-0";
|
|
58
58
|
|
|
59
59
|
return `${baseClasses} ${positionClasses}`.trim();
|
|
@@ -11,118 +11,56 @@
|
|
|
11
11
|
trigger,
|
|
12
12
|
...restProps
|
|
13
13
|
}: Props & { children?: any; trigger?: any } = $props();
|
|
14
|
+
|
|
15
|
+
// Get positioning classes based on placement
|
|
16
|
+
const placementClasses = $derived(() => {
|
|
17
|
+
const base = "absolute z-dropdown min-w-[12rem]";
|
|
18
|
+
const positioning = {
|
|
19
|
+
"bottom-start": "top-full left-0 mt-2",
|
|
20
|
+
"bottom-end": "top-full right-0 mt-2",
|
|
21
|
+
"top-start": "bottom-full left-0 mb-2",
|
|
22
|
+
"top-end": "bottom-full right-0 mb-2",
|
|
23
|
+
};
|
|
24
|
+
return `${base} ${positioning[placement]}`;
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// Get transform classes based on placement and open state
|
|
28
|
+
const transformClasses = $derived(() => {
|
|
29
|
+
if (!isOpen) {
|
|
30
|
+
const hiddenTransform = {
|
|
31
|
+
"bottom-start": "translate-y-1",
|
|
32
|
+
"bottom-end": "translate-y-1",
|
|
33
|
+
"top-start": "-translate-y-1",
|
|
34
|
+
"top-end": "-translate-y-1",
|
|
35
|
+
};
|
|
36
|
+
return `opacity-0 invisible ${hiddenTransform[placement]}`;
|
|
37
|
+
}
|
|
38
|
+
return "opacity-100 visible translate-y-0";
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Get dropdown content classes
|
|
42
|
+
const dropdownContentClasses = $derived(() => {
|
|
43
|
+
return `
|
|
44
|
+
${placementClasses()}
|
|
45
|
+
bg-brand-100
|
|
46
|
+
rounded-lg
|
|
47
|
+
shadow-lg
|
|
48
|
+
border-0
|
|
49
|
+
py-2
|
|
50
|
+
transition-all
|
|
51
|
+
duration-200
|
|
52
|
+
ease-in-out
|
|
53
|
+
${transformClasses()}
|
|
54
|
+
`.trim().replace(/\s+/g, " ");
|
|
55
|
+
});
|
|
14
56
|
</script>
|
|
15
57
|
|
|
16
|
-
<div
|
|
17
|
-
class="dropdown-container group relative inline-block"
|
|
18
|
-
data-placement={placement}
|
|
19
|
-
>
|
|
58
|
+
<div class="relative inline-block" data-placement={placement}>
|
|
20
59
|
{@render trigger?.()}
|
|
21
60
|
|
|
22
61
|
{#if isOpen}
|
|
23
|
-
<div
|
|
24
|
-
class="dropdown-content opacity-100 visible transform-none group-hover:opacity-100 group-hover:visible group-focus-within:opacity-100 group-focus-within:visible"
|
|
25
|
-
>
|
|
62
|
+
<div class={dropdownContentClasses()}>
|
|
26
63
|
{@render children?.()}
|
|
27
64
|
</div>
|
|
28
65
|
{/if}
|
|
29
66
|
</div>
|
|
30
|
-
|
|
31
|
-
<style>
|
|
32
|
-
.dropdown-container {
|
|
33
|
-
position: relative;
|
|
34
|
-
display: inline-block;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
.dropdown-content {
|
|
38
|
-
position: absolute;
|
|
39
|
-
z-index: 50;
|
|
40
|
-
background-color: white;
|
|
41
|
-
border: 1px solid #e5e7eb;
|
|
42
|
-
border-radius: 0.5rem;
|
|
43
|
-
box-shadow:
|
|
44
|
-
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
|
45
|
-
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
46
|
-
min-width: 12rem;
|
|
47
|
-
opacity: 0;
|
|
48
|
-
visibility: hidden;
|
|
49
|
-
transition:
|
|
50
|
-
opacity 0.2s ease-in-out,
|
|
51
|
-
visibility 0.2s ease-in-out,
|
|
52
|
-
transform 0.2s ease-in-out;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Positioning based on data-placement */
|
|
56
|
-
.dropdown-container[data-placement="bottom-start"] .dropdown-content {
|
|
57
|
-
top: calc(100% + 8px);
|
|
58
|
-
left: 0;
|
|
59
|
-
transform: translateY(4px);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
.dropdown-container[data-placement="bottom-end"] .dropdown-content {
|
|
63
|
-
top: calc(100% + 8px);
|
|
64
|
-
right: 0;
|
|
65
|
-
transform: translateY(4px);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.dropdown-container[data-placement="top-start"] .dropdown-content {
|
|
69
|
-
bottom: calc(100% + 8px);
|
|
70
|
-
left: 0;
|
|
71
|
-
transform: translateY(-4px);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.dropdown-container[data-placement="top-end"] .dropdown-content {
|
|
75
|
-
bottom: calc(100% + 8px);
|
|
76
|
-
right: 0;
|
|
77
|
-
transform: translateY(-4px);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/* Show on hover/focus */
|
|
81
|
-
.dropdown-container:hover .dropdown-content,
|
|
82
|
-
.dropdown-container:focus-within .dropdown-content {
|
|
83
|
-
opacity: 1;
|
|
84
|
-
visibility: visible;
|
|
85
|
-
transform: translateY(0);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/* Arrow styling */
|
|
89
|
-
.dropdown-content::before {
|
|
90
|
-
content: "";
|
|
91
|
-
position: absolute;
|
|
92
|
-
width: 0;
|
|
93
|
-
height: 0;
|
|
94
|
-
border-style: solid;
|
|
95
|
-
border-width: 6px;
|
|
96
|
-
border-color: transparent;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.dropdown-container[data-placement="bottom-start"]
|
|
100
|
-
.dropdown-content::before,
|
|
101
|
-
.dropdown-container[data-placement="bottom-end"] .dropdown-content::before {
|
|
102
|
-
top: -6px;
|
|
103
|
-
border-bottom-color: white;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
.dropdown-container[data-placement="top-start"] .dropdown-content::before,
|
|
107
|
-
.dropdown-container[data-placement="top-end"] .dropdown-content::before {
|
|
108
|
-
bottom: -6px;
|
|
109
|
-
border-top-color: white;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.dropdown-container[data-placement="bottom-start"]
|
|
113
|
-
.dropdown-content::before {
|
|
114
|
-
left: 1rem;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
.dropdown-container[data-placement="bottom-end"] .dropdown-content::before {
|
|
118
|
-
right: 1rem;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
.dropdown-container[data-placement="top-start"] .dropdown-content::before {
|
|
122
|
-
left: 1rem;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
.dropdown-container[data-placement="top-end"] .dropdown-content::before {
|
|
126
|
-
right: 1rem;
|
|
127
|
-
}
|
|
128
|
-
</style>
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
let fileInput = $state<HTMLInputElement>();
|
|
24
24
|
|
|
25
25
|
function handleFileSelect(event: Event) {
|
|
26
|
+
if (disabled) return;
|
|
27
|
+
|
|
26
28
|
const input = event.target as HTMLInputElement;
|
|
27
29
|
if (!input.files || input.files.length === 0) return;
|
|
28
30
|
|
|
@@ -36,6 +38,8 @@
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function removeImage() {
|
|
41
|
+
if (disabled) return;
|
|
42
|
+
|
|
39
43
|
if (value && typeof URL !== "undefined" && URL.revokeObjectURL) {
|
|
40
44
|
URL.revokeObjectURL(value);
|
|
41
45
|
}
|
|
@@ -43,6 +47,7 @@
|
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
function triggerFileSelect() {
|
|
50
|
+
if (disabled) return;
|
|
46
51
|
fileInput?.click();
|
|
47
52
|
}
|
|
48
53
|
</script>
|
|
@@ -54,10 +59,10 @@
|
|
|
54
59
|
<img
|
|
55
60
|
src={value}
|
|
56
61
|
alt=""
|
|
57
|
-
class="w-full h-32 object-cover rounded-lg border
|
|
62
|
+
class="w-full h-32 object-cover rounded-lg border-0"
|
|
58
63
|
/>
|
|
59
64
|
<div
|
|
60
|
-
class="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
|
|
65
|
+
class="absolute inset-0 bg-black/50 dark:bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center"
|
|
61
66
|
>
|
|
62
67
|
<div class="flex gap-2">
|
|
63
68
|
<Button
|
|
@@ -82,11 +87,15 @@
|
|
|
82
87
|
{:else}
|
|
83
88
|
<!-- Empty State -->
|
|
84
89
|
<div
|
|
85
|
-
class="border-2 border-dashed border-
|
|
90
|
+
class="border-2 border-dashed border-stone-200 rounded-lg p-6 text-center hover:border-brand-500 transition-colors {disabled
|
|
91
|
+
? 'cursor-not-allowed opacity-50'
|
|
92
|
+
: 'cursor-pointer'}"
|
|
86
93
|
onclick={triggerFileSelect}
|
|
87
94
|
role="button"
|
|
88
|
-
tabindex=
|
|
89
|
-
onkeydown={(e) =>
|
|
95
|
+
tabindex={disabled ? -1 : 0}
|
|
96
|
+
onkeydown={(e) =>
|
|
97
|
+
e.key === "Enter" && !disabled && triggerFileSelect()}
|
|
98
|
+
aria-disabled={disabled}
|
|
90
99
|
>
|
|
91
100
|
<div class="space-y-3">
|
|
92
101
|
<div
|
|
@@ -110,6 +119,7 @@
|
|
|
110
119
|
type="file"
|
|
111
120
|
{accept}
|
|
112
121
|
onchange={handleFileSelect}
|
|
122
|
+
{disabled}
|
|
113
123
|
class="hidden"
|
|
114
124
|
/>
|
|
115
125
|
</div>
|