@makolabs/ripple 3.2.0 → 3.4.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/dist/elements/alert/Alert.svelte +63 -11
- package/dist/forms/Textarea.svelte +2 -0
- package/dist/forms/form-types.d.ts +6 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/layout/card/card-types.d.ts +34 -8
- package/dist/layout/comment-composer/CommentComposer.svelte +111 -0
- package/dist/layout/comment-composer/CommentComposer.svelte.d.ts +4 -0
- package/dist/layout/comment-composer/comment-composer-types.d.ts +99 -0
- package/dist/layout/comment-composer/comment-composer-types.js +1 -0
- package/dist/layout/comment-composer/comment-composer.d.ts +76 -0
- package/dist/layout/comment-composer/comment-composer.js +47 -0
- package/package.json +1 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { cn } from '../../helper/cls.js';
|
|
3
3
|
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
-
import { Color } from '../../variants.js';
|
|
4
|
+
import { Color, Size } from '../../variants.js';
|
|
5
5
|
import { fade } from 'svelte/transition';
|
|
6
6
|
import { quintOut } from 'svelte/easing';
|
|
7
|
+
import Button from '../../button/Button.svelte';
|
|
7
8
|
import type { AlertProps, VariantColors } from '../../index.js';
|
|
8
9
|
|
|
9
10
|
let {
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
message,
|
|
12
13
|
color = Color.DEFAULT,
|
|
13
14
|
onclose,
|
|
14
|
-
|
|
15
|
+
actions,
|
|
15
16
|
icon: Icon,
|
|
16
17
|
class: className = '',
|
|
17
18
|
testId
|
|
@@ -21,6 +22,9 @@
|
|
|
21
22
|
onclose?.();
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
// Container chrome: bg-{c}-50 + border-{c}-200 + body text-{c}-800.
|
|
26
|
+
// Title and icon get their own steps for stronger hierarchy:
|
|
27
|
+
// title → text-{c}-900 (darker than body), icon circle → bg-{c}-100 / text-{c}-700.
|
|
24
28
|
const colorClasses: Record<VariantColors, string> = {
|
|
25
29
|
[Color.DEFAULT]: 'bg-default-50 border-default-200 text-default-800',
|
|
26
30
|
[Color.PRIMARY]: 'bg-primary-50 border-primary-200 text-primary-800',
|
|
@@ -30,28 +34,79 @@
|
|
|
30
34
|
[Color.WARNING]: 'bg-warning-50 border-warning-200 text-warning-800',
|
|
31
35
|
[Color.DANGER]: 'bg-danger-50 border-danger-200 text-danger-800'
|
|
32
36
|
};
|
|
37
|
+
|
|
38
|
+
const titleColorClasses: Record<VariantColors, string> = {
|
|
39
|
+
[Color.DEFAULT]: 'text-default-900',
|
|
40
|
+
[Color.PRIMARY]: 'text-primary-900',
|
|
41
|
+
[Color.SECONDARY]: 'text-secondary-900',
|
|
42
|
+
[Color.INFO]: 'text-info-900',
|
|
43
|
+
[Color.SUCCESS]: 'text-success-900',
|
|
44
|
+
[Color.WARNING]: 'text-warning-900',
|
|
45
|
+
[Color.DANGER]: 'text-danger-900'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const iconCircleClasses: Record<VariantColors, string> = {
|
|
49
|
+
[Color.DEFAULT]: 'bg-default-100 text-default-700',
|
|
50
|
+
[Color.PRIMARY]: 'bg-primary-100 text-primary-700',
|
|
51
|
+
[Color.SECONDARY]: 'bg-secondary-100 text-secondary-700',
|
|
52
|
+
[Color.INFO]: 'bg-info-100 text-info-700',
|
|
53
|
+
[Color.SUCCESS]: 'bg-success-100 text-success-700',
|
|
54
|
+
[Color.WARNING]: 'bg-warning-100 text-warning-700',
|
|
55
|
+
[Color.DANGER]: 'bg-danger-100 text-danger-700'
|
|
56
|
+
};
|
|
33
57
|
</script>
|
|
34
58
|
|
|
35
59
|
<div
|
|
36
60
|
role="alert"
|
|
37
|
-
class={cn('relative rounded-lg border p-4', colorClasses[color], className)}
|
|
61
|
+
class={cn('relative rounded-lg border p-4 shadow-sm', colorClasses[color], className)}
|
|
38
62
|
data-testid={buildTestId('alert', undefined, testId)}
|
|
39
63
|
transition:fade={{ duration: 1000, easing: quintOut }}
|
|
40
64
|
>
|
|
41
|
-
<div class="flex items-
|
|
65
|
+
<div class="flex items-center justify-between gap-4">
|
|
42
66
|
{#if Icon}
|
|
43
|
-
<
|
|
67
|
+
<div
|
|
68
|
+
class={cn(
|
|
69
|
+
'flex h-10 w-10 shrink-0 items-center justify-center rounded-full',
|
|
70
|
+
iconCircleClasses[color]
|
|
71
|
+
)}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
data-testid={buildTestId('alert', 'icon', testId)}
|
|
74
|
+
>
|
|
75
|
+
<Icon class="h-5 w-5" />
|
|
76
|
+
</div>
|
|
44
77
|
{/if}
|
|
45
|
-
<div class="flex-1">
|
|
78
|
+
<div class="min-w-0 flex-1">
|
|
46
79
|
{#if title}
|
|
47
|
-
<h4
|
|
80
|
+
<h4
|
|
81
|
+
class={cn('font-semibold', titleColorClasses[color])}
|
|
82
|
+
data-testid={buildTestId('alert', 'title', testId)}
|
|
83
|
+
>
|
|
84
|
+
{title}
|
|
85
|
+
</h4>
|
|
48
86
|
{/if}
|
|
49
87
|
<p class="text-sm" data-testid={buildTestId('alert', 'message', testId)}>{message}</p>
|
|
50
88
|
</div>
|
|
89
|
+
{#if actions && actions.length > 0}
|
|
90
|
+
<div
|
|
91
|
+
class="flex shrink-0 items-center gap-2"
|
|
92
|
+
data-testid={buildTestId('alert', 'actions', testId)}
|
|
93
|
+
>
|
|
94
|
+
{#each actions as action, i (action.label + i)}
|
|
95
|
+
<Button
|
|
96
|
+
size={Size.SM}
|
|
97
|
+
variant={action.variant ?? 'solid'}
|
|
98
|
+
color={action.color ?? color}
|
|
99
|
+
onclick={() => action.onclick?.()}
|
|
100
|
+
>
|
|
101
|
+
{action.label}
|
|
102
|
+
</Button>
|
|
103
|
+
{/each}
|
|
104
|
+
</div>
|
|
105
|
+
{/if}
|
|
51
106
|
{#if onclose}
|
|
52
107
|
<button
|
|
53
108
|
type="button"
|
|
54
|
-
class="hover:bg-default-200/20
|
|
109
|
+
class="hover:bg-default-200/20 shrink-0 cursor-pointer rounded-md p-1"
|
|
55
110
|
onclick={handleClose}
|
|
56
111
|
aria-label="Close alert"
|
|
57
112
|
data-testid={buildTestId('alert', 'close', testId)}
|
|
@@ -66,7 +121,4 @@
|
|
|
66
121
|
</button>
|
|
67
122
|
{/if}
|
|
68
123
|
</div>
|
|
69
|
-
{#if footer}
|
|
70
|
-
{@render footer()}
|
|
71
|
-
{/if}
|
|
72
124
|
</div>
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
class: className = '',
|
|
24
24
|
oninput,
|
|
25
25
|
onblur,
|
|
26
|
+
onkeydown,
|
|
26
27
|
testId
|
|
27
28
|
}: TextareaProps = $props();
|
|
28
29
|
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
{value}
|
|
104
105
|
oninput={handleInput}
|
|
105
106
|
onblur={handleBlur}
|
|
107
|
+
{onkeydown}
|
|
106
108
|
></textarea>
|
|
107
109
|
|
|
108
110
|
{#if showCount && maxLength !== undefined}
|
|
@@ -468,6 +468,12 @@ export interface TextareaProps {
|
|
|
468
468
|
oninput?: (value: string) => void;
|
|
469
469
|
/** Fires on blur with the current value. */
|
|
470
470
|
onblur?: (value: string) => void;
|
|
471
|
+
/**
|
|
472
|
+
* Raw keydown handler on the underlying `<textarea>`. Useful for
|
|
473
|
+
* keyboard shortcuts like ⌘/Ctrl+Enter to submit. The event is
|
|
474
|
+
* forwarded as-is so callers can `preventDefault()` when needed.
|
|
475
|
+
*/
|
|
476
|
+
onkeydown?: (event: KeyboardEvent) => void;
|
|
471
477
|
testId?: string;
|
|
472
478
|
}
|
|
473
479
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type { ModalProps } from './modal/modal-types.js';
|
|
|
28
28
|
export type { ConfirmDialogProps } from './modal/confirm-dialog-types.js';
|
|
29
29
|
export type { DrawerProps } from './drawer/drawer-types.js';
|
|
30
30
|
export type { DropdownItem, DropSection, DropHeaderConfig, DropdownMenuProps, SelectItem, SelectProps } from './elements/dropdown/dropdown-types.js';
|
|
31
|
-
export type { CardProps, AlertProps, MetricDetail, MetricCardProps } from './layout/card/card-types.js';
|
|
31
|
+
export type { CardProps, AlertProps, AlertAction, MetricDetail, MetricCardProps } from './layout/card/card-types.js';
|
|
32
32
|
export type { DataRow, KeyType, StatusType, TableColumn, SortDirection, SortState, TableProps } from './layout/table/table-types.js';
|
|
33
33
|
export type { BreadcrumbItem, BreadcrumbsProps, PageHeaderProps } from './header/header-types.js';
|
|
34
34
|
export type { TabItem, TabProps, TabsGroupProps, TabContentProps } from './layout/tabs/tabs-types.js';
|
|
@@ -58,6 +58,7 @@ export type { FilterTab, FilterGroup, FilterSelectionValue, DatePreset, DateRang
|
|
|
58
58
|
export { defaultDatePresets, toIsoDate, fromIsoDate } from './filters/date-presets.js';
|
|
59
59
|
export type { StepperProps, StepperStep, StepState, StepperOrientation } from './layout/stepper/stepper-types.js';
|
|
60
60
|
export type { ActivityItemBadge, ActivityItemAction, ActivityItem, ActivityListProps, ActivityListSize } from './layout/activity-list/activity-list-types.js';
|
|
61
|
+
export type { CommentComposerProps, CommentComposerSize } from './layout/comment-composer/comment-composer-types.js';
|
|
61
62
|
export type { FileUploadProps, FileUploadSize, FilePreviewProps, UploadedFile, StagedFile } from './elements/file-upload/file-upload-types.js';
|
|
62
63
|
export type { ChatMessageType, StreamingCallback, ChatAction, ChatMessage, ChatResponse, QuickAction, FileBrowserProps } from './ai/ai-types.js';
|
|
63
64
|
export type { GetUsersOptions, GetUsersResult, UserEmail, UserPhone, User, Permission, Role, UserTableProps, UserModalProps, UserModalSavePayload, UserViewModalProps, UserApproveModalProps, UserManagementAdapter, UserManagementProps, FormErrors } from './user-management/user-management-types.js';
|
|
@@ -90,6 +91,7 @@ export { default as Sidebar } from './layout/sidebar/Sidebar.svelte';
|
|
|
90
91
|
export { default as NavItem } from './layout/sidebar/NavItem.svelte';
|
|
91
92
|
export { default as NavGroup } from './layout/sidebar/NavGroup.svelte';
|
|
92
93
|
export { default as ActivityList } from './layout/activity-list/ActivityList.svelte';
|
|
94
|
+
export { default as CommentComposer } from './layout/comment-composer/CommentComposer.svelte';
|
|
93
95
|
export { default as Stepper } from './layout/stepper/Stepper.svelte';
|
|
94
96
|
export { default as Progress } from './elements/progress/Progress.svelte';
|
|
95
97
|
export { default as Spinner } from './elements/spinner/Spinner.svelte';
|
package/dist/index.js
CHANGED
|
@@ -62,6 +62,8 @@ export { default as NavItem } from './layout/sidebar/NavItem.svelte';
|
|
|
62
62
|
export { default as NavGroup } from './layout/sidebar/NavGroup.svelte';
|
|
63
63
|
// Elements - ActivityList
|
|
64
64
|
export { default as ActivityList } from './layout/activity-list/ActivityList.svelte';
|
|
65
|
+
// Elements - CommentComposer
|
|
66
|
+
export { default as CommentComposer } from './layout/comment-composer/CommentComposer.svelte';
|
|
65
67
|
// Elements - Stepper
|
|
66
68
|
export { default as Stepper } from './layout/stepper/Stepper.svelte';
|
|
67
69
|
// Elements - Progress
|
|
@@ -73,6 +73,26 @@ export type CardProps = {
|
|
|
73
73
|
loading?: boolean;
|
|
74
74
|
testId?: string;
|
|
75
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Button-style action attached to an `<Alert>` (e.g. "Mark as Shipped",
|
|
78
|
+
* "Upgrade now"). Rendered inline on the right side of the alert,
|
|
79
|
+
* between the message and the optional close button.
|
|
80
|
+
*
|
|
81
|
+
* The same shape as `ActivityItemAction` — consumers building both
|
|
82
|
+
* activity feeds and alerts only need to learn one action contract.
|
|
83
|
+
*/
|
|
84
|
+
export type AlertAction = {
|
|
85
|
+
label: string;
|
|
86
|
+
onclick?: () => void;
|
|
87
|
+
/**
|
|
88
|
+
* Override the action button's color. Defaults to the parent
|
|
89
|
+
* `<Alert>`'s `color` so the CTA visually anchors to the banner
|
|
90
|
+
* (e.g. a warning alert gets warning-colored buttons).
|
|
91
|
+
*/
|
|
92
|
+
color?: VariantColors;
|
|
93
|
+
/** Optional ripple button variant. @default 'solid' */
|
|
94
|
+
variant?: 'solid' | 'outline' | 'ghost' | 'link';
|
|
95
|
+
};
|
|
76
96
|
/**
|
|
77
97
|
* Props for `<Alert>` — a prominent inline banner for success / warning /
|
|
78
98
|
* error / info messages. Use at the top of a page or above a form. For
|
|
@@ -86,12 +106,14 @@ export type CardProps = {
|
|
|
86
106
|
*
|
|
87
107
|
* @example
|
|
88
108
|
* ```svelte
|
|
89
|
-
* <!-- Dismissable with
|
|
90
|
-
* <Alert
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
109
|
+
* <!-- Dismissable with an inline action -->
|
|
110
|
+
* <Alert
|
|
111
|
+
* color="warning"
|
|
112
|
+
* title="Ready for Shipment"
|
|
113
|
+
* message="Payment complete. Notify supplier when dispatched."
|
|
114
|
+
* actions={[{ label: 'Mark as Shipped', onclick: openShipModal }]}
|
|
115
|
+
* onclose={() => {}}
|
|
116
|
+
* />
|
|
95
117
|
* ```
|
|
96
118
|
*/
|
|
97
119
|
export type AlertProps = {
|
|
@@ -104,8 +126,12 @@ export type AlertProps = {
|
|
|
104
126
|
class?: ClassValue;
|
|
105
127
|
/** When provided, renders a × dismiss button. */
|
|
106
128
|
onclose?: () => void;
|
|
107
|
-
/**
|
|
108
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Inline action buttons rendered on the right side of the alert,
|
|
131
|
+
* between the message and the close button. Each action defaults
|
|
132
|
+
* to the alert's `color` and `variant: 'solid'`.
|
|
133
|
+
*/
|
|
134
|
+
actions?: AlertAction[];
|
|
109
135
|
/** Override the default color-appropriate icon. */
|
|
110
136
|
icon?: Component;
|
|
111
137
|
testId?: string;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { cn } from '../../helper/cls.js';
|
|
3
|
+
import { buildTestId } from '../../helper/testid.js';
|
|
4
|
+
import { commentComposer, avatarCircleClasses } from './comment-composer.js';
|
|
5
|
+
import Card from '../card/Card.svelte';
|
|
6
|
+
import Textarea from '../../forms/Textarea.svelte';
|
|
7
|
+
import Button from '../../button/Button.svelte';
|
|
8
|
+
import { Color, Size } from '../../variants.js';
|
|
9
|
+
import type { CommentComposerProps } from '../../index.js';
|
|
10
|
+
|
|
11
|
+
let {
|
|
12
|
+
value = $bindable(''),
|
|
13
|
+
placeholder = 'Add a comment…',
|
|
14
|
+
submitLabel = 'Post',
|
|
15
|
+
avatar,
|
|
16
|
+
avatarInitials,
|
|
17
|
+
avatarColor = Color.DEFAULT,
|
|
18
|
+
disabled = false,
|
|
19
|
+
loading = false,
|
|
20
|
+
size = 'md',
|
|
21
|
+
rows = 3,
|
|
22
|
+
maxLength,
|
|
23
|
+
showCount = false,
|
|
24
|
+
class: className = '',
|
|
25
|
+
testId,
|
|
26
|
+
onsubmit
|
|
27
|
+
}: CommentComposerProps = $props();
|
|
28
|
+
|
|
29
|
+
const slots = $derived(commentComposer({ size }));
|
|
30
|
+
const cardSize = $derived(size === 'sm' ? Size.SM : Size.MD);
|
|
31
|
+
const textareaSize = $derived(size === 'sm' ? Size.SM : Size.MD);
|
|
32
|
+
const buttonSize = $derived(size === 'sm' ? Size.XS : Size.SM);
|
|
33
|
+
|
|
34
|
+
const trimmed = $derived((value ?? '').trim());
|
|
35
|
+
const canSubmit = $derived(trimmed.length > 0 && !disabled && !loading);
|
|
36
|
+
const avatarTint = $derived(avatarCircleClasses[avatarColor] ?? avatarCircleClasses.default);
|
|
37
|
+
|
|
38
|
+
function submit() {
|
|
39
|
+
if (!canSubmit) return;
|
|
40
|
+
onsubmit?.(trimmed);
|
|
41
|
+
value = '';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
45
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
submit();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<Card size={cardSize} bodyClass="p-0" class={cn(className)} {testId}>
|
|
53
|
+
<div
|
|
54
|
+
class={slots.body()}
|
|
55
|
+
role="group"
|
|
56
|
+
aria-label="Comment composer"
|
|
57
|
+
data-testid={buildTestId('comment-composer', undefined, testId)}
|
|
58
|
+
>
|
|
59
|
+
<div
|
|
60
|
+
class={cn(slots.avatarWrapper(), !avatar && avatarTint)}
|
|
61
|
+
data-testid={buildTestId('comment-composer', 'avatar', testId)}
|
|
62
|
+
aria-hidden={avatar ? undefined : 'true'}
|
|
63
|
+
>
|
|
64
|
+
{#if avatar}
|
|
65
|
+
{@render avatar()}
|
|
66
|
+
{:else if avatarInitials}
|
|
67
|
+
<span class={slots.avatarInitials()}>{avatarInitials}</span>
|
|
68
|
+
{:else}
|
|
69
|
+
<svg
|
|
70
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
71
|
+
viewBox="0 0 16 16"
|
|
72
|
+
class={size === 'sm' ? 'size-3.5' : 'size-4'}
|
|
73
|
+
fill="currentColor"
|
|
74
|
+
aria-hidden="true"
|
|
75
|
+
>
|
|
76
|
+
<path
|
|
77
|
+
d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6Zm-5.5 6.5c0-2.485 2.462-4.5 5.5-4.5s5.5 2.015 5.5 4.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5Z"
|
|
78
|
+
/>
|
|
79
|
+
</svg>
|
|
80
|
+
{/if}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<div class={slots.main()}>
|
|
84
|
+
<Textarea
|
|
85
|
+
name={buildTestId('comment-composer', 'field', testId)}
|
|
86
|
+
bind:value
|
|
87
|
+
{placeholder}
|
|
88
|
+
{rows}
|
|
89
|
+
{maxLength}
|
|
90
|
+
{showCount}
|
|
91
|
+
disabled={disabled || loading}
|
|
92
|
+
size={textareaSize}
|
|
93
|
+
onkeydown={handleKeydown}
|
|
94
|
+
testId={buildTestId('comment-composer', undefined, testId)}
|
|
95
|
+
/>
|
|
96
|
+
<div class={slots.footer()}>
|
|
97
|
+
<Button
|
|
98
|
+
size={buttonSize}
|
|
99
|
+
color={Color.PRIMARY}
|
|
100
|
+
variant="solid"
|
|
101
|
+
disabled={!canSubmit}
|
|
102
|
+
{loading}
|
|
103
|
+
onclick={submit}
|
|
104
|
+
testId={buildTestId('comment-composer', 'submit', testId)}
|
|
105
|
+
>
|
|
106
|
+
{submitLabel}
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</Card>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { ClassValue } from 'tailwind-variants';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { VariantColors } from '../../index.js';
|
|
4
|
+
/** Density variants for `<CommentComposer>`. Mirrors `ActivityListSize`. */
|
|
5
|
+
export type CommentComposerSize = 'sm' | 'md';
|
|
6
|
+
/**
|
|
7
|
+
* Props for `<CommentComposer>` — a comment input surface meant to sit
|
|
8
|
+
* directly under an `<ActivityList>` (or any feed/timeline). Composes
|
|
9
|
+
* Card + Textarea + Button so the styling matches the rest of ripple.
|
|
10
|
+
*
|
|
11
|
+
* State is self-managed by default (`value` is bindable and clears
|
|
12
|
+
* after a successful `onsubmit`). Bind `value` externally if you need
|
|
13
|
+
* full control.
|
|
14
|
+
*
|
|
15
|
+
* Submit triggers: clicking the action button, or pressing
|
|
16
|
+
* ⌘/Ctrl+Enter while the textarea is focused.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```svelte
|
|
20
|
+
* <CommentComposer
|
|
21
|
+
* avatarInitials="BB"
|
|
22
|
+
* avatarColor="primary"
|
|
23
|
+
* onsubmit={(text) => savedComments.push({ text, at: new Date() })}
|
|
24
|
+
* />
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```svelte
|
|
29
|
+
* <!-- Custom avatar (e.g., img or icon component) -->
|
|
30
|
+
* <CommentComposer onsubmit={post}>
|
|
31
|
+
* {#snippet avatar()}
|
|
32
|
+
* <img src={user.photoUrl} alt="" class="size-8 rounded-full" />
|
|
33
|
+
* {/snippet}
|
|
34
|
+
* </CommentComposer>
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export type CommentComposerProps = {
|
|
38
|
+
/** Bindable comment text. Cleared after a successful submit unless externally bound. @default '' */
|
|
39
|
+
value?: string;
|
|
40
|
+
/** Placeholder shown in the textarea. @default 'Add a comment…' */
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
/** Label for the submit button. @default 'Post' */
|
|
43
|
+
submitLabel?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Custom avatar slot. When provided, it wins over `avatarInitials`.
|
|
46
|
+
* Use this to render an `<img>`, an icon component, or any markup.
|
|
47
|
+
* The composer reserves a 32px (md) / 24px (sm) square slot — render
|
|
48
|
+
* accordingly.
|
|
49
|
+
*/
|
|
50
|
+
avatar?: Snippet;
|
|
51
|
+
/**
|
|
52
|
+
* Fallback initials shown in a colored circle when no `avatar`
|
|
53
|
+
* snippet is provided. Typically 1–2 characters.
|
|
54
|
+
*/
|
|
55
|
+
avatarInitials?: string;
|
|
56
|
+
/**
|
|
57
|
+
* Tints the initials circle. Has no effect when `avatar` is
|
|
58
|
+
* provided. @default 'default'
|
|
59
|
+
*/
|
|
60
|
+
avatarColor?: VariantColors;
|
|
61
|
+
/** Disables both the textarea and the submit button. */
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Marks the composer as in-flight. Disables interaction and renders
|
|
65
|
+
* the submit button in its loading state (ripple's `Button` already
|
|
66
|
+
* supports this).
|
|
67
|
+
*/
|
|
68
|
+
loading?: boolean;
|
|
69
|
+
/** Visual density. @default 'md' */
|
|
70
|
+
size?: CommentComposerSize;
|
|
71
|
+
/** Initial textarea row count. @default 3 */
|
|
72
|
+
rows?: number;
|
|
73
|
+
/** Optional max character length, passed through to the underlying `<Textarea>`. */
|
|
74
|
+
maxLength?: number;
|
|
75
|
+
/** Show a "n / max" counter. Requires `maxLength`. @default false */
|
|
76
|
+
showCount?: boolean;
|
|
77
|
+
class?: ClassValue;
|
|
78
|
+
/**
|
|
79
|
+
* Test ID prefix. When set, the component emits these selectors
|
|
80
|
+
* (all built via `buildTestId`):
|
|
81
|
+
* - `{testId}-card` — root card wrapper (from `<Card>`)
|
|
82
|
+
* - `{testId}-comment-composer` — composer body
|
|
83
|
+
* - `{testId}-comment-composer-avatar` — avatar slot
|
|
84
|
+
* - `{testId}-comment-composer-textarea` — `<textarea>` element
|
|
85
|
+
* - `{testId}-comment-composer-textarea-wrapper` — textarea wrapper div
|
|
86
|
+
* - `{testId}-comment-composer-submit-button` — submit button
|
|
87
|
+
*
|
|
88
|
+
* When `testId` is unset the same selectors emit without the prefix
|
|
89
|
+
* (e.g. `comment-composer-submit-button`). Pass a unique `testId`
|
|
90
|
+
* if you render multiple composers on the same page.
|
|
91
|
+
*/
|
|
92
|
+
testId?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Fires when the user submits a non-empty trimmed comment, either
|
|
95
|
+
* via the submit button or ⌘/Ctrl+Enter. The composer auto-clears
|
|
96
|
+
* `value` after this fires (unless externally bound).
|
|
97
|
+
*/
|
|
98
|
+
onsubmit?: (text: string) => void;
|
|
99
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare const commentComposer: import("tailwind-variants").TVReturnType<{
|
|
2
|
+
size: {
|
|
3
|
+
md: {
|
|
4
|
+
body: string;
|
|
5
|
+
avatarWrapper: string;
|
|
6
|
+
main: string;
|
|
7
|
+
footer: string;
|
|
8
|
+
};
|
|
9
|
+
sm: {
|
|
10
|
+
body: string;
|
|
11
|
+
avatarWrapper: string;
|
|
12
|
+
avatarInitials: string;
|
|
13
|
+
main: string;
|
|
14
|
+
footer: string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}, {
|
|
18
|
+
body: string;
|
|
19
|
+
avatarWrapper: string;
|
|
20
|
+
avatarInitials: string;
|
|
21
|
+
main: string;
|
|
22
|
+
textareaSlot: string;
|
|
23
|
+
footer: string;
|
|
24
|
+
}, undefined, {
|
|
25
|
+
size: {
|
|
26
|
+
md: {
|
|
27
|
+
body: string;
|
|
28
|
+
avatarWrapper: string;
|
|
29
|
+
main: string;
|
|
30
|
+
footer: string;
|
|
31
|
+
};
|
|
32
|
+
sm: {
|
|
33
|
+
body: string;
|
|
34
|
+
avatarWrapper: string;
|
|
35
|
+
avatarInitials: string;
|
|
36
|
+
main: string;
|
|
37
|
+
footer: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
}, {
|
|
41
|
+
body: string;
|
|
42
|
+
avatarWrapper: string;
|
|
43
|
+
avatarInitials: string;
|
|
44
|
+
main: string;
|
|
45
|
+
textareaSlot: string;
|
|
46
|
+
footer: string;
|
|
47
|
+
}, import("tailwind-variants").TVReturnType<{
|
|
48
|
+
size: {
|
|
49
|
+
md: {
|
|
50
|
+
body: string;
|
|
51
|
+
avatarWrapper: string;
|
|
52
|
+
main: string;
|
|
53
|
+
footer: string;
|
|
54
|
+
};
|
|
55
|
+
sm: {
|
|
56
|
+
body: string;
|
|
57
|
+
avatarWrapper: string;
|
|
58
|
+
avatarInitials: string;
|
|
59
|
+
main: string;
|
|
60
|
+
footer: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}, {
|
|
64
|
+
body: string;
|
|
65
|
+
avatarWrapper: string;
|
|
66
|
+
avatarInitials: string;
|
|
67
|
+
main: string;
|
|
68
|
+
textareaSlot: string;
|
|
69
|
+
footer: string;
|
|
70
|
+
}, undefined, unknown, unknown, undefined>>;
|
|
71
|
+
/**
|
|
72
|
+
* Tailwind classes for the avatar fallback circle, keyed by ripple
|
|
73
|
+
* VariantColor. Mirrors `iconCircleClasses` in activity-list.ts so
|
|
74
|
+
* a composer placed under an ActivityList visually matches its rows.
|
|
75
|
+
*/
|
|
76
|
+
export declare const avatarCircleClasses: Record<string, string>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tv } from 'tailwind-variants';
|
|
2
|
+
// Item-level slots only. The card chrome (border, radius, shadow) is
|
|
3
|
+
// delegated to <Card>, matching how ActivityList composes Card.
|
|
4
|
+
export const commentComposer = tv({
|
|
5
|
+
slots: {
|
|
6
|
+
body: 'flex',
|
|
7
|
+
avatarWrapper: 'shrink-0 inline-flex items-center justify-center rounded-full ring-4 ring-white',
|
|
8
|
+
avatarInitials: 'text-xs font-medium leading-none uppercase select-none',
|
|
9
|
+
main: 'flex-1 min-w-0',
|
|
10
|
+
textareaSlot: '',
|
|
11
|
+
footer: 'flex items-center justify-end'
|
|
12
|
+
},
|
|
13
|
+
variants: {
|
|
14
|
+
size: {
|
|
15
|
+
md: {
|
|
16
|
+
body: 'gap-3 p-4',
|
|
17
|
+
avatarWrapper: 'size-8 mt-1',
|
|
18
|
+
main: 'space-y-3',
|
|
19
|
+
footer: 'gap-2'
|
|
20
|
+
},
|
|
21
|
+
sm: {
|
|
22
|
+
body: 'gap-2 p-3',
|
|
23
|
+
avatarWrapper: 'size-6 mt-0.5',
|
|
24
|
+
avatarInitials: 'text-[10px]',
|
|
25
|
+
main: 'space-y-2',
|
|
26
|
+
footer: 'gap-1.5'
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
size: 'md'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Tailwind classes for the avatar fallback circle, keyed by ripple
|
|
36
|
+
* VariantColor. Mirrors `iconCircleClasses` in activity-list.ts so
|
|
37
|
+
* a composer placed under an ActivityList visually matches its rows.
|
|
38
|
+
*/
|
|
39
|
+
export const avatarCircleClasses = {
|
|
40
|
+
default: 'bg-default-100 text-default-600',
|
|
41
|
+
primary: 'bg-primary-100 text-primary-600',
|
|
42
|
+
secondary: 'bg-secondary-100 text-secondary-600',
|
|
43
|
+
info: 'bg-info-100 text-info-600',
|
|
44
|
+
success: 'bg-success-100 text-success-700',
|
|
45
|
+
warning: 'bg-warning-100 text-warning-700',
|
|
46
|
+
danger: 'bg-danger-100 text-danger-700'
|
|
47
|
+
};
|