@joewinke/jatui 0.1.0
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/package.json +46 -0
- package/src/lib/components/AudioWaveform.svelte +694 -0
- package/src/lib/components/AvailabilityModal.svelte +173 -0
- package/src/lib/components/Badge.svelte +38 -0
- package/src/lib/components/BookingForm.svelte +276 -0
- package/src/lib/components/Button.svelte +72 -0
- package/src/lib/components/CalendarPicker.svelte +284 -0
- package/src/lib/components/Card.svelte +67 -0
- package/src/lib/components/CharacterCounter.svelte +82 -0
- package/src/lib/components/ChipInput.svelte +596 -0
- package/src/lib/components/ColorSelector.svelte +163 -0
- package/src/lib/components/ConfirmModal.svelte +75 -0
- package/src/lib/components/CountdownTimer.svelte +94 -0
- package/src/lib/components/DateRangePicker.svelte +192 -0
- package/src/lib/components/Drawer.svelte +110 -0
- package/src/lib/components/FilterDropdown.svelte +202 -0
- package/src/lib/components/ImageUpload.svelte +97 -0
- package/src/lib/components/InlineEdit.svelte +283 -0
- package/src/lib/components/LazyImage.svelte +122 -0
- package/src/lib/components/LoadingSpinner.svelte +102 -0
- package/src/lib/components/Modal.svelte +208 -0
- package/src/lib/components/PhoneInput.svelte +92 -0
- package/src/lib/components/ResizableDivider.svelte +305 -0
- package/src/lib/components/ResizablePanel.svelte +302 -0
- package/src/lib/components/SearchDropdown.svelte +341 -0
- package/src/lib/components/SelectInput.svelte +215 -0
- package/src/lib/components/SignaturePad.svelte +171 -0
- package/src/lib/components/SortDropdown.svelte +148 -0
- package/src/lib/components/Sparkline.svelte +107 -0
- package/src/lib/components/SpeechForm.svelte +114 -0
- package/src/lib/components/StatusBadge.svelte +155 -0
- package/src/lib/components/TextArea.svelte +143 -0
- package/src/lib/components/TextInput.svelte +108 -0
- package/src/lib/components/ThemeSelector.svelte +195 -0
- package/src/lib/components/TimeSlotPicker.svelte +162 -0
- package/src/lib/components/VoicePlayer.svelte +420 -0
- package/src/lib/components/messaging/Avatar.svelte +81 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
- package/src/lib/components/messaging/ChannelList.svelte +107 -0
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
- package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
- package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
- package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
- package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
- package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/src/lib/components/messaging/MessageInput.svelte +451 -0
- package/src/lib/components/messaging/MessageItem.svelte +338 -0
- package/src/lib/components/messaging/MessageThread.svelte +306 -0
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
- package/src/lib/components/messaging/StartDMModal.svelte +100 -0
- package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
- package/src/lib/index.ts +185 -0
- package/src/lib/types/booking.ts +143 -0
- package/src/lib/types/messaging.ts +459 -0
- package/src/lib/utils/currency.ts +20 -0
- package/src/lib/utils/daisyuiColors.ts +243 -0
- package/src/lib/utils/dateFormatters.ts +153 -0
- package/src/lib/utils/mentionParser.ts +188 -0
- package/src/lib/utils/phoneFormat.ts +74 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SpeechForm — Text-to-speech generation form with voice selection, character limit, and audio preview.
|
|
4
|
+
*
|
|
5
|
+
* Voice options are configurable via the `voiceOptions` prop. Includes a textarea with character counter,
|
|
6
|
+
* voice selector dropdown, optional audio preview player, and submit/cancel buttons.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let {
|
|
10
|
+
text = $bindable(""),
|
|
11
|
+
voice = $bindable("en-US-female-27"),
|
|
12
|
+
previewUrl = null,
|
|
13
|
+
isCreating = false,
|
|
14
|
+
maxLength = 1000,
|
|
15
|
+
voiceOptions = [
|
|
16
|
+
{ id: "en-US-female-27", label: "US Female (Clear)" },
|
|
17
|
+
{ id: "en-US-male-27", label: "US Male (Clear)" },
|
|
18
|
+
{ id: "en-GB-female-11", label: "UK Female" },
|
|
19
|
+
{ id: "en-GB-male-10", label: "UK Male" },
|
|
20
|
+
{ id: "en-US-female-24", label: "US Female (Warm)" },
|
|
21
|
+
{ id: "en-US-male-24", label: "US Male (Deep)" },
|
|
22
|
+
],
|
|
23
|
+
infoText = "The system will convert your text to natural-sounding speech which you can download and use.",
|
|
24
|
+
onSubmit,
|
|
25
|
+
onCancel,
|
|
26
|
+
} = $props<{
|
|
27
|
+
text?: string
|
|
28
|
+
voice?: string
|
|
29
|
+
previewUrl?: string | null
|
|
30
|
+
isCreating?: boolean
|
|
31
|
+
maxLength?: number
|
|
32
|
+
voiceOptions?: { id: string; label: string }[]
|
|
33
|
+
infoText?: string
|
|
34
|
+
onSubmit: () => void
|
|
35
|
+
onCancel: () => void
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
let remaining = $derived(maxLength - (text?.length || 0))
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
return () => {
|
|
42
|
+
if (previewUrl) {
|
|
43
|
+
URL.revokeObjectURL(previewUrl)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="space-y-4">
|
|
50
|
+
<div class="form-control">
|
|
51
|
+
<label class="label" for="speech-text-input">
|
|
52
|
+
<span class="label-text font-medium">Text to Convert</span>
|
|
53
|
+
<span class="label-text-alt">{remaining} characters left</span>
|
|
54
|
+
</label>
|
|
55
|
+
<textarea
|
|
56
|
+
id="speech-text-input"
|
|
57
|
+
bind:value={text}
|
|
58
|
+
placeholder="Enter the text you want to convert to speech..."
|
|
59
|
+
class="textarea min-h-32"
|
|
60
|
+
maxlength={maxLength}
|
|
61
|
+
disabled={isCreating}
|
|
62
|
+
></textarea>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="form-control">
|
|
66
|
+
<label class="label" for="speech-voice-select">
|
|
67
|
+
<span class="label-text font-medium">Voice</span>
|
|
68
|
+
</label>
|
|
69
|
+
<select id="speech-voice-select" bind:value={voice} class="select" disabled={isCreating}>
|
|
70
|
+
{#each voiceOptions as option}
|
|
71
|
+
<option value={option.id}>{option.label}</option>
|
|
72
|
+
{/each}
|
|
73
|
+
</select>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{#if previewUrl}
|
|
77
|
+
<div class="form-control">
|
|
78
|
+
<div class="label">
|
|
79
|
+
<span class="label-text font-medium">Preview</span>
|
|
80
|
+
</div>
|
|
81
|
+
<audio src={previewUrl} controls class="w-full" aria-label="Generated speech preview">
|
|
82
|
+
Your browser does not support the audio element.
|
|
83
|
+
</audio>
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
|
|
87
|
+
{#if infoText}
|
|
88
|
+
<div class="flex items-center gap-2 text-info bg-info/10 p-3 rounded">
|
|
89
|
+
<!-- Info icon -->
|
|
90
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
|
91
|
+
<p class="text-sm">{infoText}</p>
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
<div class="flex justify-end gap-2 mt-6">
|
|
97
|
+
<button class="btn btn-ghost" onclick={onCancel} disabled={isCreating}>
|
|
98
|
+
Cancel
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
class="btn btn-primary gap-2"
|
|
102
|
+
onclick={onSubmit}
|
|
103
|
+
disabled={isCreating || !text}
|
|
104
|
+
>
|
|
105
|
+
{#if isCreating}
|
|
106
|
+
<span class="loading loading-spinner loading-sm"></span>
|
|
107
|
+
Converting...
|
|
108
|
+
{:else}
|
|
109
|
+
<!-- Mic icon -->
|
|
110
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" x2="12" y1="19" y2="22"/></svg>
|
|
111
|
+
Generate Speech
|
|
112
|
+
{/if}
|
|
113
|
+
</button>
|
|
114
|
+
</div>
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* StatusBadge Component
|
|
4
|
+
*
|
|
5
|
+
* Predefined status badge with semantic colors, icons, and dot indicators.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type StatusValue =
|
|
9
|
+
| 'active'
|
|
10
|
+
| 'inactive'
|
|
11
|
+
| 'pending'
|
|
12
|
+
| 'completed'
|
|
13
|
+
| 'cancelled'
|
|
14
|
+
| 'draft'
|
|
15
|
+
| 'scheduled'
|
|
16
|
+
| 'in-progress'
|
|
17
|
+
| 'overdue'
|
|
18
|
+
| 'paid'
|
|
19
|
+
| 'unpaid'
|
|
20
|
+
| 'partial'
|
|
21
|
+
| 'assigned'
|
|
22
|
+
| 'available';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
status: StatusValue;
|
|
26
|
+
size?: 'xs' | 'sm' | 'md' | 'lg';
|
|
27
|
+
showIcon?: boolean;
|
|
28
|
+
showDot?: boolean;
|
|
29
|
+
customText?: string;
|
|
30
|
+
clickable?: boolean;
|
|
31
|
+
outlined?: boolean;
|
|
32
|
+
class?: string;
|
|
33
|
+
ariaLabel?: string;
|
|
34
|
+
onclick?: (event: { event: MouseEvent; status: string }) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
status,
|
|
39
|
+
size = 'md',
|
|
40
|
+
showIcon = false,
|
|
41
|
+
showDot = false,
|
|
42
|
+
customText = '',
|
|
43
|
+
clickable = false,
|
|
44
|
+
outlined = false,
|
|
45
|
+
class: className = '',
|
|
46
|
+
ariaLabel = '',
|
|
47
|
+
onclick
|
|
48
|
+
}: Props = $props();
|
|
49
|
+
|
|
50
|
+
const statusConfig: Record<string, { text: string; variant: string; icon: string }> = {
|
|
51
|
+
active: { text: 'Active', variant: 'success', icon: '✓' },
|
|
52
|
+
inactive: { text: 'Inactive', variant: 'error', icon: '✗' },
|
|
53
|
+
pending: { text: 'Pending', variant: 'warning', icon: '⏳' },
|
|
54
|
+
completed: { text: 'Completed', variant: 'success', icon: '✓' },
|
|
55
|
+
cancelled: { text: 'Cancelled', variant: 'error', icon: '✗' },
|
|
56
|
+
draft: { text: 'Draft', variant: 'default', icon: '📝' },
|
|
57
|
+
scheduled: { text: 'Scheduled', variant: 'info', icon: '📅' },
|
|
58
|
+
'in-progress': { text: 'In Progress', variant: 'primary', icon: '⏱️' },
|
|
59
|
+
overdue: { text: 'Overdue', variant: 'error', icon: '⚠️' },
|
|
60
|
+
paid: { text: 'Paid', variant: 'success', icon: '💰' },
|
|
61
|
+
unpaid: { text: 'Unpaid', variant: 'warning', icon: '💸' },
|
|
62
|
+
partial: { text: 'Partial', variant: 'warning', icon: '📊' },
|
|
63
|
+
assigned: { text: 'Assigned', variant: 'success', icon: '✓' },
|
|
64
|
+
available: { text: 'Available', variant: 'info', icon: '⭕' }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const config = $derived(statusConfig[status] || { text: status || 'Unknown', variant: 'default', icon: '?' });
|
|
68
|
+
const displayText = $derived(customText || config.text);
|
|
69
|
+
|
|
70
|
+
const variantMap: Record<string, string> = {
|
|
71
|
+
default: 'badge-ghost',
|
|
72
|
+
primary: 'badge-primary',
|
|
73
|
+
secondary: 'badge-secondary',
|
|
74
|
+
accent: 'badge-accent',
|
|
75
|
+
success: 'badge-success',
|
|
76
|
+
warning: 'badge-warning',
|
|
77
|
+
error: 'badge-error',
|
|
78
|
+
info: 'badge-info'
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const outlinedVariantMap: Record<string, string> = {
|
|
82
|
+
default: 'badge-outline',
|
|
83
|
+
primary: 'badge-primary badge-outline',
|
|
84
|
+
secondary: 'badge-secondary badge-outline',
|
|
85
|
+
accent: 'badge-accent badge-outline',
|
|
86
|
+
success: 'badge-success badge-outline',
|
|
87
|
+
warning: 'badge-warning badge-outline',
|
|
88
|
+
error: 'badge-error badge-outline',
|
|
89
|
+
info: 'badge-info badge-outline'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const variantClass = $derived(outlined ? outlinedVariantMap[config.variant] : variantMap[config.variant]);
|
|
93
|
+
const sizeClass = $derived({ xs: 'badge-xs', sm: 'badge-sm', md: 'badge-md', lg: 'badge-lg' }[size]);
|
|
94
|
+
|
|
95
|
+
const dotColor = $derived(
|
|
96
|
+
{
|
|
97
|
+
default: 'bg-base-content',
|
|
98
|
+
primary: 'bg-primary',
|
|
99
|
+
secondary: 'bg-secondary',
|
|
100
|
+
accent: 'bg-accent',
|
|
101
|
+
success: 'bg-success',
|
|
102
|
+
warning: 'bg-warning',
|
|
103
|
+
error: 'bg-error',
|
|
104
|
+
info: 'bg-info'
|
|
105
|
+
}[config.variant]
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
const badgeClass = $derived(
|
|
109
|
+
['badge', variantClass, sizeClass, clickable ? 'cursor-pointer hover:opacity-80' : '', className]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(' ')
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
function handleClick(event: MouseEvent) {
|
|
115
|
+
if (clickable && onclick) {
|
|
116
|
+
onclick({ event, status });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
121
|
+
if (clickable && onclick && (event.key === 'Enter' || event.key === ' ')) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
onclick({ event: event as any, status });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
|
|
128
|
+
{#if clickable}
|
|
129
|
+
<span
|
|
130
|
+
class={badgeClass}
|
|
131
|
+
role="button"
|
|
132
|
+
tabindex="0"
|
|
133
|
+
aria-label={ariaLabel || `Status: ${displayText}`}
|
|
134
|
+
onclick={handleClick}
|
|
135
|
+
onkeydown={handleKeyDown}
|
|
136
|
+
>
|
|
137
|
+
{#if showDot}
|
|
138
|
+
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5"></span>
|
|
139
|
+
{/if}
|
|
140
|
+
{#if showIcon}
|
|
141
|
+
<span class="mr-1">{config.icon}</span>
|
|
142
|
+
{/if}
|
|
143
|
+
{displayText}
|
|
144
|
+
</span>
|
|
145
|
+
{:else}
|
|
146
|
+
<span class={badgeClass} aria-label={ariaLabel || `Status: ${displayText}`}>
|
|
147
|
+
{#if showDot}
|
|
148
|
+
<span class="w-2 h-2 rounded-full {dotColor} mr-1.5"></span>
|
|
149
|
+
{/if}
|
|
150
|
+
{#if showIcon}
|
|
151
|
+
<span class="mr-1">{config.icon}</span>
|
|
152
|
+
{/if}
|
|
153
|
+
{displayText}
|
|
154
|
+
</span>
|
|
155
|
+
{/if}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TextArea Component
|
|
4
|
+
*
|
|
5
|
+
* Multi-line text input with validation, auto-resize, and character count.
|
|
6
|
+
* Converted from Svelte 4 to Svelte 5 runes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label?: string;
|
|
11
|
+
value?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
rows?: number;
|
|
14
|
+
maxlength?: number;
|
|
15
|
+
minlength?: number;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
readonly?: boolean;
|
|
19
|
+
autoResize?: boolean;
|
|
20
|
+
error?: string;
|
|
21
|
+
helpText?: string;
|
|
22
|
+
showCount?: boolean;
|
|
23
|
+
size?: 'sm' | 'md' | 'lg';
|
|
24
|
+
class?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
resize?: 'none' | 'vertical' | 'horizontal' | 'both';
|
|
28
|
+
oninput?: (value: string) => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
label = '',
|
|
33
|
+
value = $bindable(''),
|
|
34
|
+
placeholder = '',
|
|
35
|
+
rows = 3,
|
|
36
|
+
maxlength,
|
|
37
|
+
minlength,
|
|
38
|
+
required = false,
|
|
39
|
+
disabled = false,
|
|
40
|
+
readonly = false,
|
|
41
|
+
autoResize = false,
|
|
42
|
+
error = '',
|
|
43
|
+
helpText = '',
|
|
44
|
+
showCount = false,
|
|
45
|
+
size = 'md',
|
|
46
|
+
class: className = '',
|
|
47
|
+
id = `textarea-${Math.random().toString(36).substr(2, 9)}`,
|
|
48
|
+
name = '',
|
|
49
|
+
resize = 'vertical',
|
|
50
|
+
oninput
|
|
51
|
+
}: Props = $props();
|
|
52
|
+
|
|
53
|
+
let textareaElement: HTMLTextAreaElement | undefined = $state();
|
|
54
|
+
|
|
55
|
+
const sizeClass = $derived({ sm: 'textarea-sm', md: '', lg: 'textarea-lg' }[size]);
|
|
56
|
+
const resizeClass = $derived(
|
|
57
|
+
{ none: 'resize-none', vertical: 'resize-y', horizontal: 'resize-x', both: 'resize' }[resize]
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const textareaClass = $derived(
|
|
61
|
+
[
|
|
62
|
+
'textarea textarea-bordered w-full',
|
|
63
|
+
sizeClass,
|
|
64
|
+
resizeClass,
|
|
65
|
+
error ? 'textarea-error' : '',
|
|
66
|
+
disabled ? 'textarea-disabled' : '',
|
|
67
|
+
className
|
|
68
|
+
]
|
|
69
|
+
.filter(Boolean)
|
|
70
|
+
.join(' ')
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const characterCount = $derived(value?.length || 0);
|
|
74
|
+
const isOverLimit = $derived(maxlength ? characterCount > maxlength : false);
|
|
75
|
+
|
|
76
|
+
function handleInput(event: Event) {
|
|
77
|
+
const target = event.target as HTMLTextAreaElement;
|
|
78
|
+
value = target.value;
|
|
79
|
+
if (autoResize) autoResizeTextarea(target);
|
|
80
|
+
oninput?.(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function autoResizeTextarea(textarea: HTMLTextAreaElement) {
|
|
84
|
+
textarea.style.height = 'auto';
|
|
85
|
+
textarea.style.height = textarea.scrollHeight + 'px';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (autoResize && textareaElement && value !== undefined) {
|
|
90
|
+
autoResizeTextarea(textareaElement);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<div class="form-control w-full">
|
|
96
|
+
{#if label}
|
|
97
|
+
<label for={id} class="label">
|
|
98
|
+
<span class="label-text font-medium">
|
|
99
|
+
{label}
|
|
100
|
+
{#if required}
|
|
101
|
+
<span class="text-error ml-1">*</span>
|
|
102
|
+
{/if}
|
|
103
|
+
</span>
|
|
104
|
+
</label>
|
|
105
|
+
{/if}
|
|
106
|
+
|
|
107
|
+
<textarea
|
|
108
|
+
bind:this={textareaElement}
|
|
109
|
+
{id}
|
|
110
|
+
{name}
|
|
111
|
+
{placeholder}
|
|
112
|
+
{rows}
|
|
113
|
+
{maxlength}
|
|
114
|
+
{minlength}
|
|
115
|
+
{required}
|
|
116
|
+
{disabled}
|
|
117
|
+
{readonly}
|
|
118
|
+
bind:value
|
|
119
|
+
oninput={handleInput}
|
|
120
|
+
class={textareaClass}
|
|
121
|
+
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
122
|
+
aria-invalid={error ? 'true' : 'false'}
|
|
123
|
+
style={autoResize ? `min-height: calc(1.5em * ${rows} + 1rem);` : ''}
|
|
124
|
+
></textarea>
|
|
125
|
+
|
|
126
|
+
{#if showCount && maxlength}
|
|
127
|
+
<div class="label">
|
|
128
|
+
<span class="label-text-alt {isOverLimit ? 'text-error' : 'text-base-content/70'}">
|
|
129
|
+
{characterCount}/{maxlength}
|
|
130
|
+
</span>
|
|
131
|
+
</div>
|
|
132
|
+
{/if}
|
|
133
|
+
|
|
134
|
+
{#if error}
|
|
135
|
+
<div class="label">
|
|
136
|
+
<span id="{id}-error" class="label-text-alt text-error" role="alert">{error}</span>
|
|
137
|
+
</div>
|
|
138
|
+
{:else if helpText}
|
|
139
|
+
<div class="label">
|
|
140
|
+
<span id="{id}-help" class="label-text-alt text-base-content/70">{helpText}</span>
|
|
141
|
+
</div>
|
|
142
|
+
{/if}
|
|
143
|
+
</div>
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* TextInput Component
|
|
4
|
+
*
|
|
5
|
+
* Labeled text input with validation, error state, help text, and character count.
|
|
6
|
+
* Converted from Svelte 4 to Svelte 5 runes.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string;
|
|
11
|
+
value?: string;
|
|
12
|
+
placeholder?: string;
|
|
13
|
+
type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
|
|
14
|
+
required?: boolean;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
readonly?: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
helpText?: string;
|
|
19
|
+
size?: 'sm' | 'md' | 'lg';
|
|
20
|
+
maxlength?: number;
|
|
21
|
+
minlength?: number;
|
|
22
|
+
pattern?: string;
|
|
23
|
+
showCount?: boolean;
|
|
24
|
+
class?: string;
|
|
25
|
+
id?: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
autocomplete?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
label,
|
|
32
|
+
value = $bindable(''),
|
|
33
|
+
placeholder = '',
|
|
34
|
+
type = 'text',
|
|
35
|
+
required = false,
|
|
36
|
+
disabled = false,
|
|
37
|
+
readonly = false,
|
|
38
|
+
error = '',
|
|
39
|
+
helpText = '',
|
|
40
|
+
size = 'md',
|
|
41
|
+
maxlength,
|
|
42
|
+
minlength,
|
|
43
|
+
pattern,
|
|
44
|
+
showCount = false,
|
|
45
|
+
class: className = '',
|
|
46
|
+
id = `text-input-${Math.random().toString(36).substr(2, 9)}`,
|
|
47
|
+
name = '',
|
|
48
|
+
autocomplete = ''
|
|
49
|
+
}: Props = $props();
|
|
50
|
+
|
|
51
|
+
const sizeClass = $derived({ sm: 'input-sm', md: '', lg: 'input-lg' }[size]);
|
|
52
|
+
|
|
53
|
+
const inputClass = $derived(
|
|
54
|
+
['input input-bordered w-full', sizeClass, error ? 'input-error' : '', disabled ? 'input-disabled' : '', className]
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' ')
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const characterCount = $derived(value?.length || 0);
|
|
60
|
+
const isOverLimit = $derived(maxlength ? characterCount > maxlength : false);
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<div class="form-control w-full">
|
|
64
|
+
<label for={id} class="label">
|
|
65
|
+
<span class="label-text font-medium">
|
|
66
|
+
{label}
|
|
67
|
+
{#if required}
|
|
68
|
+
<span class="text-error ml-1">*</span>
|
|
69
|
+
{/if}
|
|
70
|
+
</span>
|
|
71
|
+
</label>
|
|
72
|
+
|
|
73
|
+
<input
|
|
74
|
+
{id}
|
|
75
|
+
{name}
|
|
76
|
+
{type}
|
|
77
|
+
{placeholder}
|
|
78
|
+
{required}
|
|
79
|
+
{disabled}
|
|
80
|
+
{readonly}
|
|
81
|
+
{maxlength}
|
|
82
|
+
{minlength}
|
|
83
|
+
{pattern}
|
|
84
|
+
{autocomplete}
|
|
85
|
+
bind:value
|
|
86
|
+
class={inputClass}
|
|
87
|
+
aria-describedby={error ? `${id}-error` : helpText ? `${id}-help` : undefined}
|
|
88
|
+
aria-invalid={error ? 'true' : 'false'}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
{#if showCount && maxlength}
|
|
92
|
+
<div class="label">
|
|
93
|
+
<span class="label-text-alt {isOverLimit ? 'text-error' : 'text-base-content/70'}">
|
|
94
|
+
{characterCount}/{maxlength}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
{#if error}
|
|
100
|
+
<div class="label">
|
|
101
|
+
<span id="{id}-error" class="label-text-alt text-error" role="alert">{error}</span>
|
|
102
|
+
</div>
|
|
103
|
+
{:else if helpText}
|
|
104
|
+
<div class="label">
|
|
105
|
+
<span id="{id}-help" class="label-text-alt text-base-content/70">{helpText}</span>
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|