@labcat2020/rocketship-svelte 0.0.0-stage → 0.2.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.
@@ -0,0 +1,124 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export type CardTag = 'article' | 'section' | 'div' | 'li'
6
+
7
+ type Props = {
8
+ as?: CardTag
9
+ class?: string
10
+ media?: Snippet
11
+ meta?: Snippet
12
+ children?: Snippet
13
+ } & HTMLAttributes<HTMLElement>
14
+
15
+ let { as = 'article', class: className = '', media, meta, children, ...attrs }: Props = $props()
16
+
17
+ const classes = $derived(['rs-card', className].filter(Boolean).join(' '))
18
+ </script>
19
+
20
+ <svelte:element this={as} {...attrs} class={classes}>
21
+ {#if media}
22
+ <div class="rs-card__media">
23
+ {@render media()}
24
+ </div>
25
+ {/if}
26
+ <div class="rs-card__body">
27
+ <div class="rs-card__content">
28
+ {@render children?.()}
29
+ </div>
30
+ {#if meta}
31
+ <div class="rs-card__meta">
32
+ {@render meta()}
33
+ </div>
34
+ {/if}
35
+ </div>
36
+ </svelte:element>
37
+
38
+ <style lang="scss">
39
+ @use '@labcat2020/rocketship/styles/mixins' as *;
40
+
41
+ :global {
42
+ .rs-card {
43
+ --rs-card-media-bg: var(--rs-color-surface-elevated, #eef2ff);
44
+ background: var(--rs-color-surface, #ffffff);
45
+ border: var(--rs-card-border-width, var(--rs-border-width-sm, 1px)) solid
46
+ var(--rs-card-border-color, var(--rs-color-border, #c8d2eb));
47
+ border-radius: var(--rs-card-border-radius, var(--rs-radius-md, 10px));
48
+ box-shadow: var(--rs-card-shadow, none);
49
+ overflow: hidden;
50
+
51
+ &__media {
52
+ background: var(--rs-card-media-bg);
53
+ min-height: var(--rs-card-media-min-height, 96px);
54
+ }
55
+
56
+ &__body {
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: var(--rs-card-body-gap, var(--rs-space-3, 12px));
60
+ padding: var(--rs-card-body-padding, var(--rs-space-4, 16px));
61
+ }
62
+
63
+ &__content {
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: var(--rs-card-content-gap, var(--rs-space-1, 4px));
67
+ min-width: 0;
68
+ }
69
+
70
+ &__meta {
71
+ display: flex;
72
+ flex-wrap: wrap;
73
+ gap: var(--rs-card-meta-gap, var(--rs-space-2, 8px));
74
+ min-width: 0;
75
+ }
76
+
77
+ &__title {
78
+ color: var(--rs-color-fg, #131a2f);
79
+ font-family: var(
80
+ --rs-font-family-sans,
81
+ 'Inter',
82
+ 'Segoe UI',
83
+ Roboto,
84
+ Helvetica,
85
+ Arial,
86
+ sans-serif
87
+ );
88
+ font-size: var(--rs-font-size-lg, #{to-rem(18)});
89
+ font-weight: var(--rs-font-weight-semibold, 600);
90
+ line-height: var(--rs-line-height-tight, 1.2);
91
+ margin: 0;
92
+ }
93
+
94
+ &__description {
95
+ color: var(--rs-color-muted, #4f5b7d);
96
+ font-family: var(
97
+ --rs-font-family-sans,
98
+ 'Inter',
99
+ 'Segoe UI',
100
+ Roboto,
101
+ Helvetica,
102
+ Arial,
103
+ sans-serif
104
+ );
105
+ font-size: var(--rs-font-size-md, #{to-rem(16)});
106
+ line-height: var(--rs-line-height-base, 1.45);
107
+ margin: 0;
108
+ }
109
+
110
+ &__token {
111
+ background: var(--rs-color-surface-elevated, #eef2ff);
112
+ border-radius: var(--rs-radius-sm, 6px);
113
+ color: var(--rs-color-muted, #4f5b7d);
114
+ display: inline-block;
115
+ font-family: var(--rs-font-family-mono, ui-monospace, monospace);
116
+ font-size: var(--rs-font-size-sm, #{to-rem(14)});
117
+ line-height: var(--rs-line-height-base, 1.45);
118
+ overflow-wrap: anywhere;
119
+ padding-block: 2px;
120
+ padding-inline: var(--rs-space-2, 8px);
121
+ }
122
+ }
123
+ }
124
+ </style>
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export type ContainerSize = 'default' | 'full' | 'wide' | 'content'
6
+ export type ContainerTag = 'div' | 'section' | 'main' | 'article' | 'header' | 'footer' | 'nav'
7
+
8
+ type Props = {
9
+ as?: ContainerTag
10
+ size?: ContainerSize
11
+ padded?: boolean
12
+ class?: string
13
+ children?: Snippet
14
+ } & HTMLAttributes<HTMLElement>
15
+
16
+ let {
17
+ as = 'div',
18
+ size = 'default',
19
+ padded = false,
20
+ class: className = '',
21
+ children,
22
+ ...attrs
23
+ }: Props = $props()
24
+
25
+ const classes = $derived(
26
+ [
27
+ 'rs-container',
28
+ size === 'full' ? 'rs-container--full-width' : undefined,
29
+ size === 'wide' ? 'rs-container--wide' : undefined,
30
+ size === 'content' ? 'rs-container--content' : undefined,
31
+ padded ? 'rs-container--padded' : undefined,
32
+ className,
33
+ ]
34
+ .filter(Boolean)
35
+ .join(' '),
36
+ )
37
+ </script>
38
+
39
+ <svelte:element this={as} {...attrs} class={classes}>
40
+ {@render children?.()}
41
+ </svelte:element>
42
+
43
+ <style lang="scss">
44
+ @use '@labcat2020/rocketship/styles/mixins' as *;
45
+
46
+ :global {
47
+ .rs-container {
48
+ container-type: inline-size;
49
+ container-name: rs-component;
50
+ --rs-container-width: var(--rs-container-width-default, 1000px);
51
+ --rs-container-gutter-left: max(env(safe-area-inset-left), var(--rs-container-gutter, 16px));
52
+ --rs-container-gutter-right: max(env(safe-area-inset-right), var(--rs-container-gutter, 16px));
53
+ --rs-container-gutters: calc(
54
+ var(--rs-container-gutter-left) + var(--rs-container-gutter-right)
55
+ );
56
+ --rs-container-gutter-top: max(
57
+ env(safe-area-inset-top),
58
+ var(--rs-container-padding-block-start, 0px)
59
+ );
60
+ --rs-container-gutter-bottom: max(
61
+ env(safe-area-inset-bottom),
62
+ var(--rs-container-padding-block-end, 0px)
63
+ );
64
+ margin-inline: auto;
65
+ max-width: calc(var(--rs-container-width) + var(--rs-container-gutters));
66
+ padding-block: var(--rs-container-gutter-top) var(--rs-container-gutter-bottom);
67
+ padding-inline: var(--rs-container-gutter-left) var(--rs-container-gutter-right);
68
+ width: 100%;
69
+
70
+ &--content {
71
+ --rs-container-width: var(--rs-container-width-content-sm, 600px);
72
+
73
+ @include media-breakpoint-up(lg) {
74
+ --rs-container-width: var(--rs-container-width-content-lg, 800px);
75
+ }
76
+ }
77
+
78
+ &--wide {
79
+ --rs-container-width: var(--rs-container-width-wide, 1200px);
80
+ }
81
+
82
+ &--full-width {
83
+ --rs-container-width: 100%;
84
+ }
85
+
86
+ &--padded {
87
+ padding-block: var(--rs-container-padding-block, 20px);
88
+ }
89
+
90
+ &--no-gutters {
91
+ padding-inline: 0;
92
+ }
93
+ }
94
+ }
95
+ </style>
@@ -0,0 +1,179 @@
1
+ <script module lang="ts">
2
+ import { createContext } from 'svelte'
3
+
4
+ /**
5
+ * A disclosure registered with an Accordion. `isOpen` lets the accordion reconcile
6
+ * panels when it switches into single-open mode, and `close` is how it collapses a
7
+ * sibling.
8
+ */
9
+ export type AccordionItem = { isOpen: () => boolean; close: () => void }
10
+
11
+ export type AccordionContext = {
12
+ register: (item: AccordionItem) => void
13
+ unregister: (item: AccordionItem) => void
14
+ opened: (item: AccordionItem) => void
15
+ }
16
+
17
+ export const [getAccordion, setAccordion, hasAccordion] = createContext<AccordionContext>()
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { untrack } from 'svelte'
22
+ import type { Snippet } from 'svelte'
23
+ import type { HTMLDetailsAttributes, ToggleEventHandler } from 'svelte/elements'
24
+
25
+ export type DisclosureTag = 'details'
26
+
27
+ type Props = {
28
+ as?: DisclosureTag
29
+ summary?: string
30
+ open?: boolean
31
+ class?: string
32
+ summarySlot?: Snippet
33
+ children?: Snippet
34
+ } & Omit<HTMLDetailsAttributes, 'open'>
35
+
36
+ let {
37
+ as = 'details',
38
+ summary,
39
+ open = $bindable(false),
40
+ class: className = '',
41
+ summarySlot,
42
+ children,
43
+ ontoggle,
44
+ ...attrs
45
+ }: Props = $props()
46
+
47
+ const accordion = hasAccordion() ? getAccordion() : undefined
48
+
49
+ const self: AccordionItem = {
50
+ isOpen: () => open,
51
+ close: () => {
52
+ open = false
53
+ },
54
+ }
55
+
56
+ $effect(() => {
57
+ if (!accordion) return
58
+ accordion.register(self)
59
+ if (untrack(() => open)) accordion.opened(self)
60
+ return () => accordion.unregister(self)
61
+ })
62
+
63
+ // `<details>` fires `toggle` once its own open state has changed, so mirroring the
64
+ // element here is what gives consumers a working `bind:open`.
65
+ const onToggle: ToggleEventHandler<HTMLDetailsElement> = (event) => {
66
+ open = event.currentTarget.open
67
+ if (open) accordion?.opened(self)
68
+ ontoggle?.(event)
69
+ }
70
+
71
+ const classes = $derived(['rs-disclosure', className].filter(Boolean).join(' '))
72
+ </script>
73
+
74
+ <svelte:element this={as} {...attrs} class={classes} open={open} ontoggle={onToggle}>
75
+ <summary class="rs-disclosure__summary">
76
+ <span class="rs-disclosure__label">
77
+ {#if summary}{summary}{:else if summarySlot}{@render summarySlot()}{/if}
78
+ </span>
79
+ <span class="rs-disclosure__indicator" aria-hidden="true"></span>
80
+ </summary>
81
+ <div class="rs-disclosure__panel">
82
+ {@render children?.()}
83
+ </div>
84
+ </svelte:element>
85
+
86
+ <style lang="scss">
87
+ @use '@labcat2020/rocketship/styles/mixins' as *;
88
+
89
+ :global {
90
+ .rs-disclosure {
91
+ --rs-disclosure-duration: var(--rs-transition-fast, 140ms);
92
+ --rs-disclosure-font-size: var(--rs-font-size-md, #{to-rem(16)});
93
+ border: var(--rs-disclosure-border-width, var(--rs-border-width-sm, 1px)) solid
94
+ var(--rs-disclosure-border-color, var(--rs-color-border, #c8d2eb));
95
+ border-radius: var(--rs-disclosure-border-radius, var(--rs-radius-md, 10px));
96
+ color: var(--rs-disclosure-color, var(--rs-color-fg, #131a2f));
97
+ font-family: var(
98
+ --rs-font-family-sans,
99
+ 'Inter',
100
+ 'Segoe UI',
101
+ Roboto,
102
+ Helvetica,
103
+ Arial,
104
+ sans-serif
105
+ );
106
+
107
+ &__summary {
108
+ align-items: center;
109
+ cursor: pointer;
110
+ display: flex;
111
+ font-size: var(--rs-disclosure-summary-font-size, var(--rs-disclosure-font-size));
112
+ font-weight: var(--rs-font-weight-medium, 500);
113
+ gap: var(--rs-space-2, 8px);
114
+ justify-content: space-between;
115
+ line-height: var(--rs-line-height-tight, 1.2);
116
+ list-style: none;
117
+ padding-block: var(--rs-disclosure-summary-padding-block, var(--rs-space-3, 12px));
118
+ padding-inline: var(--rs-disclosure-summary-padding-inline, var(--rs-space-4, 16px));
119
+
120
+ &::-webkit-details-marker {
121
+ display: none;
122
+ }
123
+
124
+ &:focus-visible {
125
+ outline: 2px solid var(--rs-color-focus-ring, #296dff);
126
+ outline-offset: -2px;
127
+ }
128
+ }
129
+
130
+ &__label {
131
+ min-width: 0;
132
+ }
133
+
134
+ &__indicator {
135
+ border-color: currentcolor;
136
+ border-style: solid;
137
+ border-width: 0 var(--rs-border-width-md, 2px) var(--rs-border-width-md, 2px) 0;
138
+ flex: none;
139
+ height: 8px;
140
+ transform: rotate(45deg);
141
+ transition: transform var(--rs-disclosure-duration) ease;
142
+ width: 8px;
143
+ }
144
+
145
+ &__panel {
146
+ font-size: var(--rs-disclosure-panel-font-size, var(--rs-disclosure-font-size));
147
+ opacity: 0;
148
+ padding-block: 0 var(--rs-disclosure-panel-padding-block, var(--rs-space-4, 16px));
149
+ padding-inline: var(--rs-disclosure-panel-padding-inline, var(--rs-space-4, 16px));
150
+ transform: translateY(-2px);
151
+ transition:
152
+ opacity var(--rs-disclosure-duration) ease-out,
153
+ transform var(--rs-disclosure-duration) ease-out;
154
+ }
155
+
156
+ &[open] {
157
+ .rs-disclosure__indicator {
158
+ transform: rotate(-135deg);
159
+ }
160
+
161
+ .rs-disclosure__panel {
162
+ opacity: 1;
163
+ transform: translateY(0);
164
+ }
165
+ }
166
+
167
+ @include container-query-up($rs-cq-sm) {
168
+ --rs-disclosure-font-size: var(--rs-font-size-lg, #{to-rem(18)});
169
+ }
170
+
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .rs-disclosure__indicator,
173
+ .rs-disclosure__panel {
174
+ transition: none;
175
+ }
176
+ }
177
+ }
178
+ }
179
+ </style>
@@ -0,0 +1,183 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+ import Label from './Label.svelte'
5
+
6
+ export type FieldTag = 'div' | 'section'
7
+
8
+ /**
9
+ * Attributes Field derives from its own props so the wrapped control stays linked to
10
+ * the label, hint, and error text. Spread it onto the control inside the `children` snippet.
11
+ */
12
+ export type FieldControlProps = {
13
+ id: string
14
+ 'aria-describedby': string | undefined
15
+ 'aria-invalid': 'true' | undefined
16
+ required: boolean | undefined
17
+ disabled: boolean | undefined
18
+ }
19
+
20
+ type Props = {
21
+ as?: FieldTag
22
+ id?: string
23
+ label?: string
24
+ hint?: string
25
+ error?: string
26
+ hintId?: string
27
+ errorId?: string
28
+ required?: boolean
29
+ optional?: boolean
30
+ requiredText?: string
31
+ optionalText?: string
32
+ disabled?: boolean
33
+ class?: string
34
+ labelSlot?: Snippet<[string]>
35
+ hintSlot?: Snippet
36
+ errorSlot?: Snippet
37
+ children?: Snippet<[FieldControlProps]>
38
+ } & HTMLAttributes<HTMLElement>
39
+
40
+ let {
41
+ as = 'div',
42
+ id: idProp,
43
+ label,
44
+ hint,
45
+ error,
46
+ hintId: hintIdProp,
47
+ errorId: errorIdProp,
48
+ required = false,
49
+ optional = false,
50
+ requiredText,
51
+ optionalText,
52
+ disabled = false,
53
+ class: className = '',
54
+ 'aria-disabled': ariaDisabled,
55
+ labelSlot,
56
+ hintSlot,
57
+ errorSlot,
58
+ children,
59
+ ...attrs
60
+ }: Props = $props()
61
+
62
+ const generatedId = $props.id()
63
+ const id = $derived(idProp ?? `rs-field-${generatedId}`)
64
+ const hintId = $derived(hintIdProp ?? `${id}-hint`)
65
+ const errorId = $derived(errorIdProp ?? `${id}-error`)
66
+ const hasLabel = $derived(Boolean(label ?? labelSlot))
67
+ const hasHint = $derived(Boolean(hint ?? hintSlot))
68
+ const hasError = $derived(Boolean(error ?? errorSlot))
69
+
70
+ const describedBy = $derived(
71
+ [hasHint ? hintId : undefined, hasError ? errorId : undefined].filter(Boolean).join(' ') ||
72
+ undefined,
73
+ )
74
+
75
+ const control = $derived({
76
+ id,
77
+ 'aria-describedby': describedBy,
78
+ 'aria-invalid': hasError ? 'true' : undefined,
79
+ required: required || undefined,
80
+ disabled: disabled || undefined,
81
+ } satisfies FieldControlProps)
82
+
83
+ const classes = $derived(
84
+ ['rs-field', disabled ? 'rs-field--disabled' : undefined, className].filter(Boolean).join(' '),
85
+ )
86
+
87
+ const wrapperAriaDisabled = $derived(disabled ? 'true' : ariaDisabled)
88
+ </script>
89
+
90
+ <svelte:element
91
+ this={as}
92
+ {...attrs}
93
+ class={classes}
94
+ aria-disabled={wrapperAriaDisabled}
95
+ >
96
+ {#if hasLabel}
97
+ <div class="rs-field__label">
98
+ {#if label}
99
+ <Label for={id} {required} {optional} {requiredText} {optionalText}>{label}</Label>
100
+ {:else if labelSlot}
101
+ {@render labelSlot(id)}
102
+ {/if}
103
+ </div>
104
+ {/if}
105
+ <div class="rs-field__control">
106
+ {@render children?.(control)}
107
+ </div>
108
+ {#if hasHint}
109
+ <div class="rs-field__hint" id={hintId}>
110
+ {#if hint}
111
+ {hint}
112
+ {:else if hintSlot}
113
+ {@render hintSlot()}
114
+ {/if}
115
+ </div>
116
+ {/if}
117
+ {#if hasError}
118
+ <div class="rs-field__error" id={errorId}>
119
+ {#if error}
120
+ {error}
121
+ {:else if errorSlot}
122
+ {@render errorSlot()}
123
+ {/if}
124
+ </div>
125
+ {/if}
126
+ </svelte:element>
127
+
128
+ <style lang="scss">
129
+ @use '@labcat2020/rocketship/styles/mixins' as *;
130
+
131
+ :global {
132
+ .rs-field {
133
+ container-name: rs-component;
134
+ container-type: inline-size;
135
+ display: flex;
136
+ flex-direction: column;
137
+ font-family: var(
138
+ --rs-font-family-sans,
139
+ 'Inter',
140
+ 'Segoe UI',
141
+ Roboto,
142
+ Helvetica,
143
+ Arial,
144
+ sans-serif
145
+ );
146
+ gap: var(--rs-field-gap, var(--rs-space-2, 8px));
147
+ min-width: 0;
148
+
149
+ @include container-query-up($rs-cq-sm) {
150
+ gap: var(--rs-field-gap, var(--rs-space-3, 12px));
151
+ }
152
+
153
+ &--disabled {
154
+ .rs-field__label,
155
+ .rs-field__control,
156
+ .rs-field__hint,
157
+ .rs-field__error {
158
+ opacity: 0.55;
159
+ }
160
+ }
161
+
162
+ &__control {
163
+ display: grid;
164
+ grid-template-columns: minmax(0, 1fr);
165
+ min-width: 0;
166
+ }
167
+
168
+ &__hint {
169
+ color: var(--rs-field-hint-color, var(--rs-color-muted, #4f5b7d));
170
+ font-size: var(--rs-field-hint-font-size, var(--rs-font-size-sm, #{to-rem(14)}));
171
+ line-height: var(--rs-line-height-base, 1.45);
172
+ margin: 0;
173
+ }
174
+
175
+ &__error {
176
+ color: var(--rs-field-error-color, var(--rs-color-danger-fg, #3d0a08));
177
+ font-size: var(--rs-field-error-font-size, var(--rs-font-size-sm, #{to-rem(14)}));
178
+ line-height: var(--rs-line-height-base, 1.45);
179
+ margin: 0;
180
+ }
181
+ }
182
+ }
183
+ </style>
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export type FigureTag = 'figure' | 'div'
6
+ export type FigureVariant = 'default' | 'bordered'
7
+ export type FigureCaptionAlign = 'start' | 'center'
8
+
9
+ type Props = {
10
+ as?: FigureTag
11
+ variant?: FigureVariant
12
+ captionAlign?: FigureCaptionAlign
13
+ caption?: string
14
+ class?: string
15
+ captionSlot?: Snippet
16
+ children?: Snippet
17
+ } & HTMLAttributes<HTMLElement>
18
+
19
+ let {
20
+ as = 'figure',
21
+ variant = 'default',
22
+ captionAlign = 'start',
23
+ caption,
24
+ class: className = '',
25
+ captionSlot,
26
+ children,
27
+ ...attrs
28
+ }: Props = $props()
29
+
30
+ const hasCaptionText = $derived(Boolean(caption?.trim()))
31
+ const hasCaption = $derived(hasCaptionText || Boolean(captionSlot))
32
+
33
+ // `<figcaption>` is only valid inside `<figure>`, so the `div` root pairs the caption
34
+ // with `role="note"` instead of rendering an orphaned caption element.
35
+ const isFigure = $derived(as === 'figure')
36
+
37
+ const classes = $derived(
38
+ ['rs-figure', variant === 'bordered' ? 'rs-figure--bordered' : undefined, className]
39
+ .filter(Boolean)
40
+ .join(' '),
41
+ )
42
+
43
+ const captionClasses = $derived(
44
+ ['rs-figure__caption', captionAlign === 'center' ? 'rs-figure__caption--center' : undefined]
45
+ .filter(Boolean)
46
+ .join(' '),
47
+ )
48
+ </script>
49
+
50
+ <svelte:element this={as} {...attrs} class={classes}>
51
+ <div class="rs-figure__media">
52
+ {@render children?.()}
53
+ </div>
54
+ {#if hasCaption}
55
+ <svelte:element
56
+ this={isFigure ? 'figcaption' : 'div'}
57
+ class={captionClasses}
58
+ role={isFigure ? undefined : 'note'}
59
+ >
60
+ {#if hasCaptionText}{caption}{/if}{#if captionSlot}{@render captionSlot()}{/if}
61
+ </svelte:element>
62
+ {/if}
63
+ </svelte:element>
64
+
65
+ <style lang="scss">
66
+ @use '@labcat2020/rocketship/styles/mixins' as *;
67
+
68
+ :global {
69
+ .rs-figure {
70
+ container-name: rs-component;
71
+ container-type: inline-size;
72
+ display: flex;
73
+ flex-direction: column;
74
+ gap: var(--rs-figure-gap, var(--rs-space-3, 12px));
75
+ margin-block: 0 var(--rs-text-spacing-bottom, 16px);
76
+ margin-inline: 0;
77
+
78
+ &__media {
79
+ border-radius: var(--rs-figure-media-radius, var(--rs-radius-md, 10px));
80
+ display: block;
81
+ overflow: hidden;
82
+
83
+ :is(img, picture, video, canvas, svg) {
84
+ display: block;
85
+ height: auto;
86
+ max-width: 100%;
87
+ width: 100%;
88
+ }
89
+
90
+ table {
91
+ margin-bottom: 0;
92
+ width: 100%;
93
+ }
94
+ }
95
+
96
+ // Only the bordered variant frames the media, so the border is declared on the
97
+ // modifier and the default variant stays unframed.
98
+ &--bordered .rs-figure__media {
99
+ border: var(--rs-figure-media-border-width, var(--rs-border-width-sm, 1px)) solid
100
+ var(--rs-figure-media-border-color, var(--rs-color-border, #c8d2eb));
101
+ }
102
+
103
+ &__caption {
104
+ color: var(--rs-figure-caption-color, var(--rs-color-muted, #4f5b7d));
105
+ font-family: var(
106
+ --rs-font-family-sans,
107
+ 'Inter',
108
+ 'Segoe UI',
109
+ Roboto,
110
+ Helvetica,
111
+ Arial,
112
+ sans-serif
113
+ );
114
+ font-size: var(--rs-figure-caption-font-size, var(--rs-font-size-sm, #{to-rem(14)}));
115
+ line-height: var(--rs-figure-caption-line-height, var(--rs-line-height-base, 1.45));
116
+ text-align: start;
117
+
118
+ @include container-query-up($rs-cq-sm) {
119
+ font-size: var(--rs-figure-caption-font-size-sm, var(--rs-font-size-sm, #{to-rem(14)}));
120
+ }
121
+
122
+ &--center {
123
+ text-align: center;
124
+ }
125
+ }
126
+ }
127
+ }
128
+ </style>