@labcat2020/rocketship-svelte 0.1.0 → 0.3.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,193 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export type TableTag = 'div' | 'section'
6
+
7
+ export type TableProps = {
8
+ as?: TableTag
9
+ striped?: boolean
10
+ compact?: boolean
11
+ hover?: boolean
12
+ caption?: string
13
+ label?: string
14
+ class?: string
15
+ children?: Snippet
16
+ } & HTMLAttributes<HTMLElement>
17
+
18
+ let {
19
+ as = 'div',
20
+ striped = false,
21
+ compact = false,
22
+ hover = false,
23
+ caption,
24
+ label,
25
+ class: className = '',
26
+ children,
27
+ ...attrs
28
+ }: TableProps = $props()
29
+
30
+ let holder = $state<HTMLElement | null>(null)
31
+ let table = $state<HTMLTableElement | null>(null)
32
+ let overflows = $state(false)
33
+
34
+ const hasCaption = $derived(Boolean(caption?.trim()))
35
+
36
+ /**
37
+ * Names the scroll region. The caption names the table, so it can name the region too; when
38
+ * there is no caption, `label` is the only hook for an accessible name.
39
+ */
40
+ const regionLabel = $derived(label?.trim() || caption?.trim() || 'Table')
41
+
42
+ const holderClasses = $derived(['rs-table-holder', className].filter(Boolean).join(' '))
43
+
44
+ /**
45
+ * The scroll region is announced only once the holder actually scrolls, and only for the keys
46
+ * the consumer left unset, so an explicit `role`, `tabindex`, or `aria-label` always wins.
47
+ */
48
+ const regionAttrs = $derived.by(() => {
49
+ if (!overflows) return {}
50
+ const region: Record<string, unknown> = {}
51
+ if (!('role' in attrs)) region.role = 'region'
52
+ if (!('tabindex' in attrs)) region.tabindex = 0
53
+ if (!('aria-label' in attrs)) region['aria-label'] = regionLabel
54
+ return region
55
+ })
56
+
57
+ const tableClasses = $derived(
58
+ [
59
+ 'rs-table',
60
+ striped ? 'rs-table--striped' : undefined,
61
+ compact ? 'rs-table--compact' : undefined,
62
+ hover ? 'rs-table--hover' : undefined,
63
+ ]
64
+ .filter(Boolean)
65
+ .join(' '),
66
+ )
67
+
68
+ /**
69
+ * A scroll container only becomes reachable by keyboard once it actually overflows, so the
70
+ * region semantics come from a measurement rather than being applied unconditionally: a
71
+ * table that fits stays out of the tab order and off the landmark list. Both the holder and
72
+ * the table are observed so a holder resize and a content resize each re-measure.
73
+ */
74
+ $effect(() => {
75
+ const holderElement = holder
76
+ const tableElement = table
77
+ if (!holderElement || !tableElement) return
78
+
79
+ const measure = () => {
80
+ overflows = holderElement.scrollWidth > holderElement.clientWidth
81
+ }
82
+
83
+ measure()
84
+
85
+ if (typeof ResizeObserver === 'undefined') return
86
+
87
+ const observer = new ResizeObserver(measure)
88
+ observer.observe(holderElement)
89
+ observer.observe(tableElement)
90
+
91
+ return () => observer.disconnect()
92
+ })
93
+ </script>
94
+
95
+ <svelte:element this={as} {...attrs} {...regionAttrs} bind:this={holder} class={holderClasses}>
96
+ <table class={tableClasses} bind:this={table}>
97
+ {#if hasCaption}
98
+ <caption>{caption}</caption>
99
+ {/if}
100
+ {@render children?.()}
101
+ </table>
102
+ </svelte:element>
103
+
104
+ <style lang="scss">
105
+ @use '@labcat2020/rocketship/styles/mixins' as *;
106
+
107
+ :global {
108
+ .rs-table-holder {
109
+ overflow-x: auto;
110
+ -webkit-overflow-scrolling: touch;
111
+ background: var(--rs-color-surface, #ffffff);
112
+ border: var(--rs-table-border-width, var(--rs-border-width-sm, 1px)) solid
113
+ var(--rs-table-border-color, var(--rs-color-border, #c8d2eb));
114
+ border-radius: var(--rs-table-border-radius, var(--rs-radius-md, 10px));
115
+
116
+ &:focus-visible {
117
+ outline: 2px solid var(--rs-table-focus-ring-color, var(--rs-color-focus-ring, #296dff));
118
+ outline-offset: 2px;
119
+ }
120
+ }
121
+
122
+ .rs-table {
123
+ border-collapse: collapse;
124
+ border-spacing: 0;
125
+ width: 100%;
126
+ font-family: var(
127
+ --rs-font-family-sans,
128
+ 'Inter',
129
+ 'Segoe UI',
130
+ Roboto,
131
+ Helvetica,
132
+ Arial,
133
+ sans-serif
134
+ );
135
+ font-size: var(--rs-table-font-size, var(--rs-font-size-sm, #{to-rem(14)}));
136
+ line-height: var(--rs-line-height-base, 1.45);
137
+ color: var(--rs-color-fg, #131a2f);
138
+ text-align: left;
139
+
140
+ th,
141
+ td {
142
+ padding-block: var(--rs-table-cell-padding-block, var(--rs-space-2, 8px));
143
+ padding-inline: var(--rs-table-cell-padding-inline, var(--rs-space-3, 12px));
144
+ text-align: left;
145
+ vertical-align: top;
146
+ border-bottom: var(--rs-table-border-width, var(--rs-border-width-sm, 1px)) solid
147
+ var(--rs-table-border-color, var(--rs-color-border, #c8d2eb));
148
+ }
149
+
150
+ thead th {
151
+ background: var(--rs-table-head-bg, var(--rs-color-surface-elevated, #eef2ff));
152
+ color: var(--rs-table-head-color, var(--rs-color-fg, #131a2f));
153
+ font-weight: var(--rs-font-weight-semibold, 600);
154
+ white-space: nowrap;
155
+ border-bottom-width: var(--rs-table-head-border-width, var(--rs-border-width-sm, 1px));
156
+ }
157
+
158
+ tbody tr:last-child td {
159
+ border-bottom: 0;
160
+ }
161
+
162
+ caption {
163
+ caption-side: top;
164
+ color: var(--rs-color-muted, #4f5b7d);
165
+ font-size: var(--rs-font-size-sm, #{to-rem(14)});
166
+ line-height: var(--rs-line-height-base, 1.45);
167
+ padding-block: var(--rs-space-2, 8px);
168
+ padding-inline: var(--rs-space-3, 12px);
169
+ text-align: left;
170
+ }
171
+
172
+ &--striped {
173
+ tbody tr:nth-child(even) {
174
+ background: var(--rs-table-striped-bg, var(--rs-color-surface-elevated, #eef2ff));
175
+ }
176
+ }
177
+
178
+ &--hover {
179
+ tbody tr:hover {
180
+ background: var(--rs-table-hover-bg, var(--rs-color-surface-elevated, #eef2ff));
181
+ }
182
+ }
183
+
184
+ &--compact {
185
+ th,
186
+ td {
187
+ padding-block: var(--rs-table-compact-padding-block, var(--rs-space-1, 4px));
188
+ padding-inline: var(--rs-table-compact-padding-inline, var(--rs-space-2, 8px));
189
+ }
190
+ }
191
+ }
192
+ }
193
+ </style>
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements'
3
+
4
+ export type TextareaSize = 'sm' | 'md' | 'lg'
5
+
6
+ type Props = {
7
+ size?: TextareaSize
8
+ invalid?: boolean
9
+ class?: string
10
+ value?: string
11
+ } & Omit<HTMLTextareaAttributes, 'value'>
12
+
13
+ let {
14
+ size = 'md',
15
+ invalid = false,
16
+ class: className = '',
17
+ value = $bindable(''),
18
+ ...attrs
19
+ }: Props = $props()
20
+
21
+ // Field passes its wiring (`id`, `aria-describedby`, `aria-invalid`, `required`, `disabled`) to
22
+ // the `children` snippet, which spreads it onto this control. An explicit attribute wins over the
23
+ // `invalid` prop, so the error state Field reports is the one the element announces and styles.
24
+ const ariaInvalid = $derived(attrs['aria-invalid'] ?? (invalid ? 'true' : undefined))
25
+ // Only the hard `true` state drives the styling; `spelling`/`grammar` are announced but not
26
+ // painted as an error, and an explicit `aria-invalid="false"` always wins over the prop.
27
+ const isInvalid = $derived(ariaInvalid === true || ariaInvalid === 'true')
28
+
29
+ const classes = $derived(
30
+ [
31
+ 'rs-textarea',
32
+ `rs-textarea--${size}`,
33
+ isInvalid ? 'rs-textarea--invalid' : undefined,
34
+ className,
35
+ ]
36
+ .filter(Boolean)
37
+ .join(' '),
38
+ )
39
+ </script>
40
+
41
+ <textarea {...attrs} aria-invalid={ariaInvalid} bind:value class={classes}></textarea>
42
+
43
+ <style lang="scss">
44
+ @use '@labcat2020/rocketship/styles/mixins' as *;
45
+
46
+ :global {
47
+ .rs-textarea {
48
+ // Size locals live on the block so a size modifier only reassigns them; `--rs-textarea-font-size`
49
+ // stays unset here so a consumer can override one control without touching the others.
50
+ --rs-textarea-font-size-sm: var(--rs-font-size-sm, #{to-rem(14)});
51
+ --rs-textarea-font-size-md: var(--rs-font-size-md, #{to-rem(16)});
52
+ --rs-textarea-font-size-lg: var(--rs-font-size-lg, #{to-rem(18)});
53
+ --rs-textarea-min-height-sm: 72px;
54
+ --rs-textarea-min-height-md: 96px;
55
+ --rs-textarea-min-height-lg: 128px;
56
+ --rs-textarea-padding-inline-sm: var(--rs-space-2, 8px);
57
+ --rs-textarea-padding-inline-md: var(--rs-space-3, 12px);
58
+ --rs-textarea-padding-inline-lg: var(--rs-space-4, 16px);
59
+ background: var(--rs-textarea-bg, var(--rs-color-surface, #ffffff));
60
+ border: var(--rs-textarea-border-width, var(--rs-border-width-sm, 1px)) solid
61
+ var(--rs-textarea-border-color, var(--rs-color-border, #c8d2eb));
62
+ border-radius: var(--rs-textarea-border-radius, var(--rs-radius-sm, 6px));
63
+ box-sizing: border-box;
64
+ color: var(--rs-textarea-color, var(--rs-color-fg, #131a2f));
65
+ display: block;
66
+ font-family: var(
67
+ --rs-font-family-sans,
68
+ 'Inter',
69
+ 'Segoe UI',
70
+ Roboto,
71
+ Helvetica,
72
+ Arial,
73
+ sans-serif
74
+ );
75
+ font-size: var(--rs-textarea-font-size, var(--rs-textarea-font-size-md));
76
+ line-height: var(--rs-line-height-base, 1.45);
77
+ min-height: var(--rs-textarea-min-height, var(--rs-textarea-min-height-md));
78
+ padding-block: var(--rs-textarea-padding-block, var(--rs-space-2, 8px));
79
+ padding-inline: var(--rs-textarea-padding-inline, var(--rs-textarea-padding-inline-md));
80
+ resize: var(--rs-textarea-resize, vertical);
81
+ transition:
82
+ background-color var(--rs-transition-fast, 140ms) ease,
83
+ border-color var(--rs-transition-fast, 140ms) ease;
84
+ width: 100%;
85
+
86
+ &::placeholder {
87
+ color: var(--rs-textarea-placeholder-color, var(--rs-color-muted, #4f5b7d));
88
+ }
89
+
90
+ &:focus-visible {
91
+ border-color: var(--rs-textarea-focus-border-color, var(--rs-color-accent, #2f5bdb));
92
+ outline: 2px solid var(--rs-textarea-focus-ring-color, var(--rs-color-focus-ring, #296dff));
93
+ outline-offset: 2px;
94
+ }
95
+
96
+ &:disabled {
97
+ cursor: not-allowed;
98
+ opacity: 0.55;
99
+ }
100
+
101
+ &--sm {
102
+ --rs-textarea-font-size: var(--rs-textarea-font-size-sm);
103
+ --rs-textarea-min-height: var(--rs-textarea-min-height-sm);
104
+ --rs-textarea-padding-inline: var(--rs-textarea-padding-inline-sm);
105
+ }
106
+
107
+ &--md {
108
+ --rs-textarea-font-size: var(--rs-textarea-font-size-md);
109
+ --rs-textarea-min-height: var(--rs-textarea-min-height-md);
110
+ --rs-textarea-padding-inline: var(--rs-textarea-padding-inline-md);
111
+ }
112
+
113
+ &--lg {
114
+ --rs-textarea-font-size: var(--rs-textarea-font-size-lg);
115
+ --rs-textarea-min-height: var(--rs-textarea-min-height-lg);
116
+ --rs-textarea-padding-inline: var(--rs-textarea-padding-inline-lg);
117
+ }
118
+
119
+ // The error state owns a coordinated colour set, so the modifier reassigns the locals the base
120
+ // rule already reads instead of restating background, border and colour.
121
+ &--invalid {
122
+ --rs-textarea-bg: var(--rs-color-danger-bg, #ffeef0);
123
+ --rs-textarea-border-color: var(--rs-color-danger-border, #f0a8b0);
124
+ --rs-textarea-color: var(--rs-color-danger-fg, #3d0a08);
125
+ }
126
+
127
+ @include container-query-up($rs-cq-sm) {
128
+ --rs-textarea-font-size-sm: var(--rs-font-size-md, #{to-rem(16)});
129
+ --rs-textarea-font-size-md: var(--rs-font-size-lg, #{to-rem(18)});
130
+ --rs-textarea-font-size-lg: var(--rs-font-size-xl, #{to-rem(20)});
131
+ }
132
+
133
+ @media (prefers-reduced-motion: reduce) {
134
+ transition: none;
135
+ }
136
+ }
137
+ }
138
+ </style>
@@ -0,0 +1,205 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte'
3
+ import type { HTMLAttributes } from 'svelte/elements'
4
+
5
+ export type TypographyTag = 'div' | 'section' | 'article'
6
+
7
+ type Props = {
8
+ as?: TypographyTag
9
+ html?: string
10
+ class?: string
11
+ children?: Snippet
12
+ } & HTMLAttributes<HTMLElement>
13
+
14
+ let { as = 'div', html, class: className = '', children, ...attrs }: Props = $props()
15
+
16
+ const classes = $derived(['rs-typography', className].filter(Boolean).join(' '))
17
+ </script>
18
+
19
+ <svelte:element this={as} {...attrs} class={classes}>
20
+ {#if html}
21
+ {@html html}
22
+ {:else}
23
+ {@render children?.()}
24
+ {/if}
25
+ </svelte:element>
26
+
27
+ <style lang="scss">
28
+ @use '@labcat2020/rocketship/styles/mixins' as *;
29
+
30
+ :global {
31
+ .rs-typography {
32
+ h1 {
33
+ @include typography-h1;
34
+ }
35
+
36
+ h2 {
37
+ @include typography-h2;
38
+ }
39
+
40
+ h3 {
41
+ @include typography-h3;
42
+ }
43
+
44
+ h4 {
45
+ @include typography-h4;
46
+ }
47
+
48
+ h5 {
49
+ @include typography-h5;
50
+ }
51
+
52
+ h6 {
53
+ @include typography-h6;
54
+ }
55
+
56
+ p {
57
+ @include typography-p;
58
+ }
59
+
60
+ a {
61
+ @include typography-a;
62
+ }
63
+
64
+ .intro-text {
65
+ @include typography-intro;
66
+ }
67
+
68
+ i,
69
+ em {
70
+ @include typography-em;
71
+ }
72
+
73
+ b,
74
+ strong {
75
+ @include typography-strong;
76
+ }
77
+
78
+ hr {
79
+ @include typography-hr;
80
+ }
81
+
82
+ li {
83
+ @include typography-li;
84
+ }
85
+
86
+ ul {
87
+ @include typography-ul;
88
+ }
89
+
90
+ ol {
91
+ @include typography-ol;
92
+ }
93
+
94
+ @include typography-list-bridges;
95
+
96
+ pre {
97
+ @include typography-pre;
98
+ }
99
+
100
+ pre code {
101
+ @include typography-pre-code;
102
+ }
103
+
104
+ code {
105
+ @include typography-code-inline;
106
+ }
107
+
108
+ kbd {
109
+ @include typography-kbd;
110
+ }
111
+
112
+ samp {
113
+ @include typography-samp;
114
+ }
115
+
116
+ blockquote {
117
+ @include typography-blockquote;
118
+ }
119
+
120
+ blockquote p {
121
+ @include typography-blockquote-p;
122
+ }
123
+
124
+ q {
125
+ @include typography-q;
126
+ }
127
+
128
+ table {
129
+ @include typography-table;
130
+ }
131
+
132
+ caption {
133
+ @include typography-caption;
134
+ }
135
+
136
+ dl {
137
+ @include typography-dl;
138
+ }
139
+
140
+ dt {
141
+ @include typography-dt;
142
+ }
143
+
144
+ dd {
145
+ @include typography-dd;
146
+ }
147
+
148
+ small {
149
+ @include typography-small;
150
+ }
151
+
152
+ sub {
153
+ @include typography-sub;
154
+ }
155
+
156
+ sup {
157
+ @include typography-sup;
158
+ }
159
+
160
+ mark {
161
+ @include typography-mark;
162
+ }
163
+
164
+ abbr[title] {
165
+ @include typography-abbr;
166
+ }
167
+
168
+ del {
169
+ @include typography-del;
170
+ }
171
+
172
+ ins {
173
+ @include typography-ins;
174
+ }
175
+
176
+ address {
177
+ @include typography-address;
178
+ }
179
+
180
+ dfn {
181
+ @include typography-dfn;
182
+ }
183
+
184
+ cite {
185
+ @include typography-cite;
186
+ }
187
+
188
+ var {
189
+ @include typography-var;
190
+ }
191
+
192
+ time {
193
+ @include typography-time;
194
+ }
195
+
196
+ > :first-child {
197
+ margin-top: 0;
198
+ }
199
+
200
+ > :last-child {
201
+ margin-bottom: 0;
202
+ }
203
+ }
204
+ }
205
+ </style>