@karbonjs/ui-svelte 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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +30 -0
  3. package/src/accordion/Accordion.svelte +63 -0
  4. package/src/alert/AlertMessage.svelte +44 -0
  5. package/src/avatar/Avatar.svelte +48 -0
  6. package/src/badge/Badge.svelte +24 -0
  7. package/src/breadcrumb/Breadcrumb.svelte +34 -0
  8. package/src/button/Button.svelte +89 -0
  9. package/src/carousel/Carousel.svelte +118 -0
  10. package/src/data/DataTable.svelte +18 -0
  11. package/src/data/Pagination.svelte +45 -0
  12. package/src/divider/Divider.svelte +27 -0
  13. package/src/dropdown/Dropdown.svelte +61 -0
  14. package/src/form/Checkbox.svelte +51 -0
  15. package/src/form/ColorPicker.svelte +95 -0
  16. package/src/form/DatePicker.svelte +196 -0
  17. package/src/form/FormInput.svelte +174 -0
  18. package/src/form/Radio.svelte +54 -0
  19. package/src/form/Select.svelte +73 -0
  20. package/src/form/Slider.svelte +74 -0
  21. package/src/form/Textarea.svelte +86 -0
  22. package/src/form/Toggle.svelte +55 -0
  23. package/src/image/Image.svelte +89 -0
  24. package/src/image/ImgZoom.svelte +96 -0
  25. package/src/index.ts +71 -0
  26. package/src/kbd/Kbd.svelte +19 -0
  27. package/src/layout/Card.svelte +67 -0
  28. package/src/layout/EmptyState.svelte +25 -0
  29. package/src/layout/PageHeader.svelte +27 -0
  30. package/src/overlay/Dialog.svelte +135 -0
  31. package/src/overlay/ImgBox.svelte +174 -0
  32. package/src/overlay/Modal.svelte +98 -0
  33. package/src/overlay/Toast.svelte +92 -0
  34. package/src/progress/Progress.svelte +50 -0
  35. package/src/skeleton/Skeleton.svelte +50 -0
  36. package/src/tabs/Tabs.svelte +59 -0
  37. package/src/tooltip/Tooltip.svelte +49 -0
@@ -0,0 +1,74 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ name: string
4
+ value?: number
5
+ min?: number
6
+ max?: number
7
+ step?: number
8
+ label?: string
9
+ showValue?: boolean
10
+ disabled?: boolean
11
+ class?: string
12
+ oninput?: (e: Event) => void
13
+ }
14
+
15
+ let {
16
+ name,
17
+ value = $bindable(0),
18
+ min = 0,
19
+ max = 100,
20
+ step = 1,
21
+ label = '',
22
+ showValue = true,
23
+ disabled = false,
24
+ class: className = '',
25
+ oninput
26
+ }: Props = $props()
27
+
28
+ const percent = $derived(((value - min) / (max - min)) * 100)
29
+ </script>
30
+
31
+ <div class="space-y-2 {className}">
32
+ {#if label || showValue}
33
+ <div class="flex items-center justify-between">
34
+ {#if label}
35
+ <label for={name} class="text-sm font-medium text-[var(--karbon-text,#1a1635)]">{label}</label>
36
+ {/if}
37
+ {#if showValue}
38
+ <span class="text-sm font-semibold text-[var(--karbon-primary)] tabular-nums">{value}</span>
39
+ {/if}
40
+ </div>
41
+ {/if}
42
+
43
+ <input
44
+ id={name}
45
+ {name}
46
+ type="range"
47
+ bind:value
48
+ {min}
49
+ {max}
50
+ {step}
51
+ {disabled}
52
+ {oninput}
53
+ class="w-full h-2 rounded-full appearance-none cursor-pointer disabled:opacity-40 disabled:cursor-not-allowed
54
+ bg-[linear-gradient(to_right,var(--karbon-primary)_{percent}%,var(--karbon-border,rgba(0,0,0,0.07))_{percent}%)]
55
+ [&::-webkit-slider-thumb]:appearance-none
56
+ [&::-webkit-slider-thumb]:w-4.5
57
+ [&::-webkit-slider-thumb]:h-4.5
58
+ [&::-webkit-slider-thumb]:rounded-full
59
+ [&::-webkit-slider-thumb]:bg-[var(--karbon-primary)]
60
+ [&::-webkit-slider-thumb]:border-2
61
+ [&::-webkit-slider-thumb]:border-white
62
+ [&::-webkit-slider-thumb]:shadow-md
63
+ [&::-webkit-slider-thumb]:transition-transform
64
+ [&::-webkit-slider-thumb]:duration-150
65
+ [&::-webkit-slider-thumb]:hover:scale-110
66
+ [&::-moz-range-thumb]:w-4
67
+ [&::-moz-range-thumb]:h-4
68
+ [&::-moz-range-thumb]:rounded-full
69
+ [&::-moz-range-thumb]:bg-[var(--karbon-primary)]
70
+ [&::-moz-range-thumb]:border-2
71
+ [&::-moz-range-thumb]:border-white
72
+ [&::-moz-range-thumb]:shadow-md"
73
+ />
74
+ </div>
@@ -0,0 +1,86 @@
1
+ <script lang="ts">
2
+ import type { FormVariant } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ name: string
6
+ value?: string
7
+ placeholder?: string
8
+ label?: string
9
+ error?: string
10
+ rows?: number
11
+ maxlength?: number
12
+ showCount?: boolean
13
+ required?: boolean
14
+ disabled?: boolean
15
+ readonly?: boolean
16
+ variant?: FormVariant
17
+ class?: string
18
+ oninput?: (e: Event) => void
19
+ }
20
+
21
+ let {
22
+ name,
23
+ value = $bindable(''),
24
+ placeholder = '',
25
+ label = '',
26
+ error = '',
27
+ rows = 4,
28
+ maxlength,
29
+ showCount = false,
30
+ required = false,
31
+ disabled = false,
32
+ readonly = false,
33
+ variant = 'dark',
34
+ class: className = '',
35
+ oninput
36
+ }: Props = $props()
37
+
38
+ const themes = {
39
+ dark: {
40
+ label: 'text-[11px] font-medium text-gray-500 uppercase tracking-wider',
41
+ input: 'border-white/8 bg-white/3 text-white placeholder-gray-700 focus:border-[var(--karbon-primary)]/50 focus:bg-white/5 focus:ring-[3px] focus:ring-[var(--karbon-primary)]/8',
42
+ error: 'text-red-400',
43
+ count: 'text-gray-600'
44
+ },
45
+ light: {
46
+ label: 'text-sm font-medium text-gray-700',
47
+ input: 'border-gray-300 bg-white text-gray-900 placeholder-gray-400 focus:border-[var(--karbon-primary)] focus:ring-2 focus:ring-[var(--karbon-primary)]/20',
48
+ error: 'text-[var(--karbon-danger)]',
49
+ count: 'text-gray-400'
50
+ }
51
+ } as const
52
+
53
+ const theme = $derived(themes[variant])
54
+ const charCount = $derived(value.length)
55
+ </script>
56
+
57
+ <div class="space-y-1.5 {className}">
58
+ {#if label}
59
+ <label for={name} class="{theme.label} block mb-1.5">{label}</label>
60
+ {/if}
61
+
62
+ <textarea
63
+ id={name}
64
+ {name}
65
+ bind:value
66
+ {placeholder}
67
+ {rows}
68
+ {maxlength}
69
+ {required}
70
+ {disabled}
71
+ {readonly}
72
+ {oninput}
73
+ class="w-full rounded-lg border px-3 py-2.5 text-[13px] md:text-sm focus:outline-none transition-all resize-y {theme.input} {error ? 'border-red-500/50' : ''}"
74
+ ></textarea>
75
+
76
+ <div class="flex items-center justify-between">
77
+ {#if error}
78
+ <p class="text-xs {theme.error}">{error}</p>
79
+ {:else}
80
+ <span></span>
81
+ {/if}
82
+ {#if showCount && maxlength}
83
+ <span class="text-xs {theme.count}">{charCount}/{maxlength}</span>
84
+ {/if}
85
+ </div>
86
+ </div>
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import type { ToggleSize } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ name: string
6
+ checked?: boolean
7
+ label?: string
8
+ size?: ToggleSize
9
+ disabled?: boolean
10
+ class?: string
11
+ onchange?: (e: Event) => void
12
+ }
13
+
14
+ let {
15
+ name,
16
+ checked = $bindable(false),
17
+ label = '',
18
+ size = 'md',
19
+ disabled = false,
20
+ class: className = '',
21
+ onchange
22
+ }: Props = $props()
23
+
24
+ const sizes = {
25
+ sm: { track: 'w-8 h-[18px]', dot: 'h-3.5 w-3.5', translate: 'translate-x-3.5' },
26
+ md: { track: 'w-10 h-[22px]', dot: 'h-4.5 w-4.5', translate: 'translate-x-4.5' }
27
+ } as const
28
+
29
+ const s = $derived(sizes[size])
30
+
31
+ function toggle() {
32
+ if (!disabled) checked = !checked
33
+ }
34
+ </script>
35
+
36
+ <label class="inline-flex items-center gap-2.5 {disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'} {className}">
37
+ <input type="checkbox" {name} bind:checked {disabled} {onchange} class="sr-only" />
38
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
39
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
40
+ <div
41
+ onclick={toggle}
42
+ class="relative inline-flex shrink-0 items-center rounded-full transition-colors duration-200
43
+ {s.track}
44
+ {checked ? 'bg-[var(--karbon-primary)]' : 'bg-[var(--karbon-border,rgba(0,0,0,0.07))]'}"
45
+ role="switch"
46
+ aria-checked={checked}
47
+ >
48
+ <span
49
+ class="inline-block rounded-full bg-white shadow-sm transition-transform duration-200 {s.dot} {checked ? s.translate : 'translate-x-0.5'}"
50
+ ></span>
51
+ </div>
52
+ {#if label}
53
+ <span class="text-sm font-medium text-[var(--karbon-text,#1a1635)] select-none">{label}</span>
54
+ {/if}
55
+ </label>
@@ -0,0 +1,89 @@
1
+ <script lang="ts">
2
+ import type { ImageHover, ImageRounded, ImageAspect } from '@karbonjs/ui-core'
3
+ import ImgBox from '../overlay/ImgBox.svelte'
4
+
5
+ interface Props {
6
+ src: string
7
+ alt?: string
8
+ hover?: ImageHover
9
+ rounded?: ImageRounded
10
+ aspect?: ImageAspect
11
+ fallback?: string
12
+ imgbox?: boolean
13
+ class?: string
14
+ onclick?: () => void
15
+ }
16
+
17
+ let {
18
+ src,
19
+ alt = '',
20
+ hover = 'none',
21
+ rounded = 'md',
22
+ aspect = 'auto',
23
+ fallback = '',
24
+ imgbox = false,
25
+ class: className = '',
26
+ onclick
27
+ }: Props = $props()
28
+
29
+ let errored = $state(false)
30
+ let imgboxOpen = $state(false)
31
+
32
+ const hoverClasses: Record<string, string> = {
33
+ none: '',
34
+ zoom: 'group-hover:scale-110',
35
+ brightness: 'group-hover:brightness-110',
36
+ blur: 'group-hover:blur-sm'
37
+ }
38
+
39
+ const roundedClasses: Record<string, string> = {
40
+ none: 'rounded-none',
41
+ sm: 'rounded',
42
+ md: 'rounded-lg',
43
+ lg: 'rounded-xl',
44
+ full: 'rounded-full'
45
+ }
46
+
47
+ const aspectClasses: Record<string, string> = {
48
+ auto: '',
49
+ square: 'aspect-square',
50
+ video: 'aspect-video',
51
+ portrait: 'aspect-[3/4]'
52
+ }
53
+
54
+ const imgSrc = $derived(errored && fallback ? fallback : src)
55
+ const isClickable = $derived(imgbox || !!onclick)
56
+
57
+ function handleClick() {
58
+ if (imgbox) imgboxOpen = true
59
+ onclick?.()
60
+ }
61
+
62
+ function handleError() {
63
+ errored = true
64
+ }
65
+ </script>
66
+
67
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
68
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
69
+ <div
70
+ class="group overflow-hidden {roundedClasses[rounded]} {aspectClasses[aspect]} {isClickable ? 'cursor-pointer' : ''} {className}"
71
+ onclick={isClickable ? handleClick : undefined}
72
+ >
73
+ <img
74
+ src={imgSrc}
75
+ {alt}
76
+ onerror={handleError}
77
+ class="w-full h-full object-cover transition-all duration-300 {hoverClasses[hover]}"
78
+ loading="lazy"
79
+ />
80
+ </div>
81
+
82
+ {#if imgbox && imgboxOpen}
83
+ <ImgBox
84
+ images={[src]}
85
+ open={imgboxOpen}
86
+ backdrop="dark"
87
+ onclose={() => imgboxOpen = false}
88
+ />
89
+ {/if}
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ import type { ImageRounded, ImgZoomTrigger } from '@karbonjs/ui-core'
3
+
4
+ interface Props {
5
+ src: string
6
+ zoomSrc?: string
7
+ alt?: string
8
+ zoom?: number
9
+ trigger?: ImgZoomTrigger
10
+ rounded?: ImageRounded
11
+ class?: string
12
+ }
13
+
14
+ let {
15
+ src,
16
+ zoomSrc,
17
+ alt = '',
18
+ zoom = 2,
19
+ trigger = 'hover',
20
+ rounded = 'md',
21
+ class: className = ''
22
+ }: Props = $props()
23
+
24
+ let container: HTMLDivElement
25
+ let zooming = $state(false)
26
+ let posX = $state(50)
27
+ let posY = $state(50)
28
+
29
+ const roundedClasses: Record<string, string> = {
30
+ none: 'rounded-none',
31
+ sm: 'rounded',
32
+ md: 'rounded-lg',
33
+ lg: 'rounded-xl',
34
+ full: 'rounded-full'
35
+ }
36
+
37
+ const zoomImage = $derived(zoomSrc || src)
38
+
39
+ function updatePosition(e: MouseEvent) {
40
+ const rect = container.getBoundingClientRect()
41
+ posX = ((e.clientX - rect.left) / rect.width) * 100
42
+ posY = ((e.clientY - rect.top) / rect.height) * 100
43
+ }
44
+
45
+ function handleMouseEnter(e: MouseEvent) {
46
+ if (trigger === 'hover') {
47
+ zooming = true
48
+ updatePosition(e)
49
+ }
50
+ }
51
+
52
+ function handleMouseMove(e: MouseEvent) {
53
+ if (zooming) updatePosition(e)
54
+ }
55
+
56
+ function handleMouseLeave() {
57
+ if (trigger === 'hover') zooming = false
58
+ }
59
+
60
+ function handleClick(e: MouseEvent) {
61
+ if (trigger === 'click') {
62
+ zooming = !zooming
63
+ if (zooming) updatePosition(e)
64
+ }
65
+ }
66
+ </script>
67
+
68
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
69
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
70
+ <div
71
+ bind:this={container}
72
+ class="relative overflow-hidden {roundedClasses[rounded]} {trigger === 'click' ? 'cursor-zoom-in' : ''} {zooming && trigger === 'click' ? 'cursor-zoom-out' : ''} {className}"
73
+ onmouseenter={handleMouseEnter}
74
+ onmousemove={handleMouseMove}
75
+ onmouseleave={handleMouseLeave}
76
+ onclick={handleClick}
77
+ >
78
+ <img
79
+ {src}
80
+ {alt}
81
+ class="w-full h-full object-cover block"
82
+ loading="lazy"
83
+ />
84
+
85
+ {#if zooming}
86
+ <div
87
+ class="absolute inset-0 pointer-events-none"
88
+ style="
89
+ background-image: url({zoomImage});
90
+ background-size: {zoom * 100}%;
91
+ background-position: {posX}% {posY}%;
92
+ background-repeat: no-repeat;
93
+ "
94
+ ></div>
95
+ {/if}
96
+ </div>
package/src/index.ts ADDED
@@ -0,0 +1,71 @@
1
+ // button
2
+ export { default as Button } from './button/Button.svelte'
3
+
4
+ // form
5
+ export { default as FormInput } from './form/FormInput.svelte'
6
+ export { default as Select } from './form/Select.svelte'
7
+ export { default as Checkbox } from './form/Checkbox.svelte'
8
+ export { default as Toggle } from './form/Toggle.svelte'
9
+ export { default as Textarea } from './form/Textarea.svelte'
10
+ export { default as Radio } from './form/Radio.svelte'
11
+ export { default as Slider } from './form/Slider.svelte'
12
+ export { default as DatePicker } from './form/DatePicker.svelte'
13
+ export { default as ColorPicker } from './form/ColorPicker.svelte'
14
+
15
+ // badge
16
+ export { default as Badge } from './badge/Badge.svelte'
17
+
18
+ // alert
19
+ export { default as AlertMessage } from './alert/AlertMessage.svelte'
20
+
21
+ // overlay
22
+ export { default as Modal } from './overlay/Modal.svelte'
23
+ export { default as Dialog } from './overlay/Dialog.svelte'
24
+ export { default as Toast } from './overlay/Toast.svelte'
25
+ export { default as ImgBox } from './overlay/ImgBox.svelte'
26
+
27
+ // layout
28
+ export { default as Card } from './layout/Card.svelte'
29
+ export { default as PageHeader } from './layout/PageHeader.svelte'
30
+ export { default as EmptyState } from './layout/EmptyState.svelte'
31
+
32
+ // image
33
+ export { default as Image } from './image/Image.svelte'
34
+ export { default as ImgZoom } from './image/ImgZoom.svelte'
35
+
36
+ // carousel
37
+ export { default as Carousel } from './carousel/Carousel.svelte'
38
+
39
+ // dropdown
40
+ export { default as Dropdown } from './dropdown/Dropdown.svelte'
41
+
42
+ // accordion
43
+ export { default as Accordion } from './accordion/Accordion.svelte'
44
+
45
+ // tabs
46
+ export { default as Tabs } from './tabs/Tabs.svelte'
47
+
48
+ // breadcrumb
49
+ export { default as Breadcrumb } from './breadcrumb/Breadcrumb.svelte'
50
+
51
+ // tooltip
52
+ export { default as Tooltip } from './tooltip/Tooltip.svelte'
53
+
54
+ // avatar
55
+ export { default as Avatar } from './avatar/Avatar.svelte'
56
+
57
+ // progress
58
+ export { default as Progress } from './progress/Progress.svelte'
59
+
60
+ // skeleton
61
+ export { default as Skeleton } from './skeleton/Skeleton.svelte'
62
+
63
+ // divider
64
+ export { default as Divider } from './divider/Divider.svelte'
65
+
66
+ // kbd
67
+ export { default as Kbd } from './kbd/Kbd.svelte'
68
+
69
+ // data
70
+ export { default as DataTable } from './data/DataTable.svelte'
71
+ export { default as Pagination } from './data/Pagination.svelte'
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ keys: string[]
4
+ class?: string
5
+ }
6
+
7
+ let { keys, class: className = '' }: Props = $props()
8
+ </script>
9
+
10
+ <span class="inline-flex items-center gap-1 {className}">
11
+ {#each keys as key, i}
12
+ {#if i > 0}
13
+ <span class="text-[var(--karbon-text-4,#b5b2cc)] text-xs">+</span>
14
+ {/if}
15
+ <kbd class="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 rounded-md border border-[var(--karbon-border,rgba(0,0,0,0.07))] bg-[var(--karbon-bg-2,#e8e6f0)] text-[var(--karbon-text-2,#5a567e)] text-[11px] font-mono font-medium shadow-sm">
16
+ {key}
17
+ </kbd>
18
+ {/each}
19
+ </span>
@@ -0,0 +1,67 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { CardVariant, CardPadding } from '@karbonjs/ui-core'
4
+
5
+ interface Props {
6
+ variant?: CardVariant
7
+ padding?: CardPadding
8
+ hoverable?: boolean
9
+ title?: string
10
+ icon?: any
11
+ class?: string
12
+ children: Snippet
13
+ header?: Snippet
14
+ footer?: Snippet
15
+ }
16
+
17
+ let {
18
+ variant = 'default',
19
+ padding = 'md',
20
+ hoverable = false,
21
+ title = '',
22
+ icon: Icon,
23
+ class: className = '',
24
+ children,
25
+ header,
26
+ footer
27
+ }: Props = $props()
28
+
29
+ const variantClasses: Record<string, string> = {
30
+ default: 'bg-[var(--karbon-bg-card,#fff)] border border-[var(--karbon-border,rgba(0,0,0,0.07))]',
31
+ elevated: 'bg-[var(--karbon-bg-card,#fff)] shadow-lg',
32
+ outlined: 'border-2 border-[var(--karbon-border,rgba(0,0,0,0.07))]',
33
+ ghost: 'bg-transparent'
34
+ }
35
+
36
+ const paddingClasses: Record<string, string> = {
37
+ none: '',
38
+ sm: 'p-3',
39
+ md: 'p-5',
40
+ lg: 'p-8'
41
+ }
42
+ </script>
43
+
44
+ <div class="rounded-xl overflow-hidden {variantClasses[variant]} {hoverable ? 'transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5' : ''} {className}">
45
+ {#if header}
46
+ <div class="px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))]">
47
+ {@render header()}
48
+ </div>
49
+ {:else if title}
50
+ <div class="flex items-center gap-2 px-5 py-3.5 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] text-[var(--karbon-text-2,#5a567e)] text-[0.825rem] font-semibold">
51
+ {#if Icon}
52
+ <Icon class="w-4 h-4" />
53
+ {/if}
54
+ <span>{title}</span>
55
+ </div>
56
+ {/if}
57
+
58
+ <div class={paddingClasses[padding]}>
59
+ {@render children()}
60
+ </div>
61
+
62
+ {#if footer}
63
+ <div class="px-5 py-3.5 border-t border-[var(--karbon-border,rgba(0,0,0,0.07))]">
64
+ {@render footer()}
65
+ </div>
66
+ {/if}
67
+ </div>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ title: string
4
+ description?: string
5
+ icon?: any
6
+ }
7
+
8
+ let {
9
+ title,
10
+ description = '',
11
+ icon: Icon
12
+ }: Props = $props()
13
+ </script>
14
+
15
+ <div class="text-center py-12 px-6">
16
+ {#if Icon}
17
+ <div class="w-14 h-14 rounded-2xl bg-[var(--karbon-nav-hover-bg,rgba(0,0,0,0.04))] text-[var(--karbon-text-4,#b5b2cc)] flex items-center justify-center mx-auto mb-4">
18
+ <Icon class="w-8 h-8" />
19
+ </div>
20
+ {/if}
21
+ <p class="text-[var(--karbon-text-2,#5a567e)] font-semibold text-[0.95rem] m-0">{title}</p>
22
+ {#if description}
23
+ <p class="text-[var(--karbon-text-3,#8e8aae)] text-[0.8rem] mt-1.5 mx-auto max-w-[22rem]">{description}</p>
24
+ {/if}
25
+ </div>
@@ -0,0 +1,27 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ title: string
4
+ description?: string
5
+ icon?: any
6
+ iconColor?: string
7
+ }
8
+
9
+ let {
10
+ title,
11
+ description = '',
12
+ icon: Icon,
13
+ iconColor = 'var(--karbon-primary, #cc1a1a)'
14
+ }: Props = $props()
15
+ </script>
16
+
17
+ <div class="flex items-start gap-3 pb-4 border-b border-[var(--karbon-border,rgba(0,0,0,0.07))] mb-1">
18
+ {#if Icon}
19
+ <Icon class="w-5 h-5 shrink-0" style="color: {iconColor}" />
20
+ {/if}
21
+ <div>
22
+ <h1 class="text-[var(--karbon-text,#1a1635)] text-[1.1rem] font-bold m-0">{title}</h1>
23
+ {#if description}
24
+ <p class="text-[var(--karbon-text-3,#8e8aae)] text-[0.8rem] mt-0.5">{description}</p>
25
+ {/if}
26
+ </div>
27
+ </div>