@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.
- package/README.md +117 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -1
- package/package.json +22 -3
- package/src/components/Accordion.svelte +89 -0
- package/src/components/Alert.svelte +129 -0
- package/src/components/Button.svelte +187 -0
- package/src/components/Card.svelte +124 -0
- package/src/components/Container.svelte +95 -0
- package/src/components/Disclosure.svelte +179 -0
- package/src/components/Field.svelte +183 -0
- package/src/components/Figure.svelte +128 -0
- package/src/components/Grid.svelte +62 -0
- package/src/components/Input.svelte +138 -0
- package/src/components/Label.svelte +92 -0
- package/src/components/Select.svelte +235 -0
- package/src/components/Table.svelte +193 -0
- package/src/components/Textarea.svelte +138 -0
- package/src/components/Typography.svelte +205 -0
package/README.md
CHANGED
|
@@ -2,7 +2,123 @@
|
|
|
2
2
|
|
|
3
3
|
Svelte 5 port of the Rocketship component library. Components reuse the same design system as the Astro package (`@labcat2020/rocketship`): design tokens, the mixin barrel, and the base resets are shared, not duplicated.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Components are either copy-style ports of their Astro originals, or Svelte-first components with no Astro original yet. Either way they render the same BEM classes and follow the same props, token, `:global` style, container-query and a11y conventions, so an Astro version can be ported back from a Svelte-first component. A Svelte-first component is provisional — it may change once the matching Astro component's platinum standard lands, and Astro is the source of truth when the two diverge.
|
|
6
|
+
|
|
7
|
+
## Components
|
|
8
|
+
|
|
9
|
+
| Component | Export | Astro original |
|
|
10
|
+
| ------------ | ----------------------------------------------------- | ------------------ |
|
|
11
|
+
| `Container` | `@labcat2020/rocketship-svelte/components/Container` | `Container.astro` |
|
|
12
|
+
| `Grid` | `@labcat2020/rocketship-svelte/components/Grid` | `Grid.astro` |
|
|
13
|
+
| `Card` | `@labcat2020/rocketship-svelte/components/Card` | `Card.astro` |
|
|
14
|
+
| `Typography` | `@labcat2020/rocketship-svelte/components/Typography` | `Typography.astro` |
|
|
15
|
+
| `Button` | `@labcat2020/rocketship-svelte/components/Button` | `Button.astro` |
|
|
16
|
+
| `Alert` | `@labcat2020/rocketship-svelte/components/Alert` | `Alert.astro` |
|
|
17
|
+
| `Table` | `@labcat2020/rocketship-svelte/components/Table` | — (Svelte-first) |
|
|
18
|
+
| `Field` | `@labcat2020/rocketship-svelte/components/Field` | — (Svelte-first) |
|
|
19
|
+
| `Label` | `@labcat2020/rocketship-svelte/components/Label` | — (Svelte-first) |
|
|
20
|
+
| `Input` | `@labcat2020/rocketship-svelte/components/Input` | — (Svelte-first) |
|
|
21
|
+
| `Textarea` | `@labcat2020/rocketship-svelte/components/Textarea` | — (Svelte-first) |
|
|
22
|
+
| `Select` | `@labcat2020/rocketship-svelte/components/Select` | — (Svelte-first) |
|
|
23
|
+
| `Disclosure` | `@labcat2020/rocketship-svelte/components/Disclosure` | — (Svelte-first) |
|
|
24
|
+
| `Accordion` | `@labcat2020/rocketship-svelte/components/Accordion` | — (Svelte-first) |
|
|
25
|
+
| `Figure` | `@labcat2020/rocketship-svelte/components/Figure` | — (Svelte-first) |
|
|
26
|
+
|
|
27
|
+
Import components from their `./components/*` subpaths. Those subpaths are the package's public surface; deep imports such as `@labcat2020/rocketship-svelte/src/components/Container.svelte` are not exported and will not resolve.
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import Container from '@labcat2020/rocketship-svelte/components/Container'
|
|
32
|
+
import Grid from '@labcat2020/rocketship-svelte/components/Grid'
|
|
33
|
+
import Card from '@labcat2020/rocketship-svelte/components/Card'
|
|
34
|
+
import Typography from '@labcat2020/rocketship-svelte/components/Typography'
|
|
35
|
+
import Button from '@labcat2020/rocketship-svelte/components/Button'
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<Container as="main" size="content" padded>
|
|
39
|
+
<Typography as="article">
|
|
40
|
+
<h1>Title</h1>
|
|
41
|
+
<p>Body copy.</p>
|
|
42
|
+
</Typography>
|
|
43
|
+
<Grid columns={3}>
|
|
44
|
+
<Card>…</Card>
|
|
45
|
+
<Card>…</Card>
|
|
46
|
+
<Card>…</Card>
|
|
47
|
+
</Grid>
|
|
48
|
+
<Button variant="primary">Continue</Button>
|
|
49
|
+
</Container>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each component spreads unknown attributes onto its rendered element and composes an incoming `class` with its BEM block.
|
|
53
|
+
|
|
54
|
+
| Component | Props |
|
|
55
|
+
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
56
|
+
| `Container` | `as` (`div`, `section`, `main`, `article`, `header`, `footer`, `nav`), `size` (`default`, `content`, `wide`, `full`), `padded`, `class` |
|
|
57
|
+
| `Grid` | `as` (`div`, `ul`, `ol`), `columns` (`2`, `3`, `4`), `class` |
|
|
58
|
+
| `Card` | `as` (`article`, `section`, `div`, `li`), `media` snippet, `meta` snippet, `class` |
|
|
59
|
+
| `Typography` | `as` (`div`, `section`, `article`), `html`, `class` |
|
|
60
|
+
| `Button` | `as` (`button`, `a`), `href`, `variant` (`primary`, `secondary`, `ghost`), `size` (`small`, `medium`, `large`), `disabled`, `type`, `class` |
|
|
61
|
+
| `Alert` | `as` (`div`, `section`, `aside`, `article`), `variant` (`info`, `success`, `warning`, `error`), `title`, `titleSlot` snippet, `role`, `class` |
|
|
62
|
+
| `Input` | `type` (`text`, `email`, `password`, `number`, `search`, `url`, `tel`), `size` (`sm`, `md`, `lg`), `invalid`, `value` (`bind:value`), `class` |
|
|
63
|
+
| `Textarea` | `size` (`sm`, `md`, `lg`), `invalid`, `value` (`bind:value`), `class` |
|
|
64
|
+
| `Select` | `size` (`sm`, `md`, `lg`), `invalid`, `placeholder`, `value` (`bind:value`), `children` snippet, `class` |
|
|
65
|
+
| `Figure` | `as` (`figure`, `div`), `variant` (`default`, `bordered`), `captionAlign` (`start`, `center`), `caption`, `captionSlot` snippet, `class` |
|
|
66
|
+
|
|
67
|
+
### `Typography` `html` prop
|
|
68
|
+
|
|
69
|
+
`Typography` keeps the `html` prop: pass a string and it renders as raw markup inside the wrapper, taking precedence over the `children` snippet. It is the Svelte equivalent of the Astro component's `set:html`.
|
|
70
|
+
|
|
71
|
+
**Sanitize before you pass it.** The prop renders trusted HTML verbatim and does not sanitize its input, so it is only safe for content you control or content already sanitized at the boundary. Never pass unsanitized user input, request data, or third-party HTML through it — run it through a sanitizer (for example DOMPurify) first, or render structured markup with the `children` snippet instead.
|
|
72
|
+
|
|
73
|
+
### `Field`, `Input` and `Textarea`
|
|
74
|
+
|
|
75
|
+
`Field` hands each control its wiring through the `children` snippet: `id`, `aria-describedby`, `aria-invalid`, `required` and `disabled`. Spread that object onto `Input` or `Textarea` so the label, hint and error text stay linked to the control.
|
|
76
|
+
|
|
77
|
+
```svelte
|
|
78
|
+
<script lang="ts">
|
|
79
|
+
import Field from '@labcat2020/rocketship-svelte/components/Field'
|
|
80
|
+
import Input from '@labcat2020/rocketship-svelte/components/Input'
|
|
81
|
+
import Textarea from '@labcat2020/rocketship-svelte/components/Textarea'
|
|
82
|
+
|
|
83
|
+
let email = $state('')
|
|
84
|
+
let notes = $state('')
|
|
85
|
+
</script>
|
|
86
|
+
|
|
87
|
+
<Field label="Work email" hint="We send deployment alerts here.">
|
|
88
|
+
{#snippet children(control)}
|
|
89
|
+
<Input {...control} type="email" bind:value={email} />
|
|
90
|
+
{/snippet}
|
|
91
|
+
</Field>
|
|
92
|
+
|
|
93
|
+
<Field label="Release notes" error="Add at least ten characters.">
|
|
94
|
+
{#snippet children(control)}
|
|
95
|
+
<Textarea {...control} rows="3" bind:value={notes} />
|
|
96
|
+
{/snippet}
|
|
97
|
+
</Field>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Both controls also work standalone. `invalid` raises `aria-invalid` and applies the error styling, and a size modifier (`sm`, `md`, `lg`) sets the type scale and control height. A `Field` error flows into the control's `aria-invalid` and its modifier, so the styling follows the error text Field renders.
|
|
101
|
+
|
|
102
|
+
### `Select`
|
|
103
|
+
|
|
104
|
+
`Select` wraps a native `<select>` and takes its options through the `children` snippet, plain `<option>` and `<optgroup>` included. There is no custom listbox, so keyboard, screen-reader and mobile behaviour are the platform's.
|
|
105
|
+
|
|
106
|
+
```svelte
|
|
107
|
+
<script lang="ts">
|
|
108
|
+
import Select from '@labcat2020/rocketship-svelte/components/Select'
|
|
109
|
+
|
|
110
|
+
let team = $state('')
|
|
111
|
+
</script>
|
|
112
|
+
|
|
113
|
+
<Select bind:value={team} placeholder="Choose a team" name="team">
|
|
114
|
+
<optgroup label="Engineering">
|
|
115
|
+
<option value="frontend">Frontend</option>
|
|
116
|
+
<option value="backend">Backend</option>
|
|
117
|
+
</optgroup>
|
|
118
|
+
</Select>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`placeholder` renders a disabled option with an empty value, so it reads as an unset field. Once a real option is chosen the placeholder cannot be selected again; a parent can still reset the field by writing `''`. `size` accepts `sm`, `md` or `lg`; a number is passed through to the native `size` attribute, which renders a listbox instead of a dropdown, in which case no chevron is drawn.
|
|
6
122
|
|
|
7
123
|
## Install
|
|
8
124
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
export
|
|
1
|
+
export { default as Accordion } from './components/Accordion.svelte';
|
|
2
|
+
export { default as Disclosure } from './components/Disclosure.svelte';
|
|
3
|
+
export { default as Field } from './components/Field.svelte';
|
|
4
|
+
export { default as Figure } from './components/Figure.svelte';
|
|
5
|
+
export { default as Input } from './components/Input.svelte';
|
|
6
|
+
export { default as Label } from './components/Label.svelte';
|
|
7
|
+
export { default as Select } from './components/Select.svelte';
|
|
8
|
+
export { default as Table } from './components/Table.svelte';
|
|
9
|
+
export { default as Textarea } from './components/Textarea.svelte';
|
|
10
|
+
export type * from './components/Accordion.svelte';
|
|
11
|
+
export type * from './components/Disclosure.svelte';
|
|
12
|
+
export type * from './components/Field.svelte';
|
|
13
|
+
export type * from './components/Figure.svelte';
|
|
14
|
+
export type * from './components/Input.svelte';
|
|
15
|
+
export type * from './components/Label.svelte';
|
|
16
|
+
export type * from './components/Select.svelte';
|
|
17
|
+
export type * from './components/Table.svelte';
|
|
18
|
+
export type * from './components/Textarea.svelte';
|
|
2
19
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAA;AACtE,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,MAAM,EAAE,MAAM,4BAA4B,CAAA;AAC9D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAA;AAClE,mBAAmB,+BAA+B,CAAA;AAClD,mBAAmB,gCAAgC,CAAA;AACnD,mBAAmB,2BAA2B,CAAA;AAC9C,mBAAmB,4BAA4B,CAAA;AAC/C,mBAAmB,2BAA2B,CAAA;AAC9C,mBAAmB,2BAA2B,CAAA;AAC9C,mBAAmB,4BAA4B,CAAA;AAC/C,mBAAmB,2BAA2B,CAAA;AAC9C,mBAAmB,8BAA8B,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
/// <reference types="svelte" />
|
|
2
|
+
export { default as Accordion } from './components/Accordion.svelte';
|
|
3
|
+
export { default as Disclosure } from './components/Disclosure.svelte';
|
|
4
|
+
export { default as Field } from './components/Field.svelte';
|
|
5
|
+
export { default as Figure } from './components/Figure.svelte';
|
|
6
|
+
export { default as Input } from './components/Input.svelte';
|
|
7
|
+
export { default as Label } from './components/Label.svelte';
|
|
8
|
+
export { default as Select } from './components/Select.svelte';
|
|
9
|
+
export { default as Table } from './components/Table.svelte';
|
|
10
|
+
export { default as Textarea } from './components/Textarea.svelte';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@labcat2020/rocketship-svelte",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Rocketship Svelte port — Svelte 5 components that reuse the Rocketship design system (tokens, mixins, base resets)",
|
|
@@ -18,19 +18,38 @@
|
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
19
19
|
"import": "./dist/index.js",
|
|
20
20
|
"default": "./dist/index.js"
|
|
21
|
-
}
|
|
21
|
+
},
|
|
22
|
+
"./components/Alert": "./src/components/Alert.svelte",
|
|
23
|
+
"./components/Accordion": "./src/components/Accordion.svelte",
|
|
24
|
+
"./components/Button": "./src/components/Button.svelte",
|
|
25
|
+
"./components/Card": "./src/components/Card.svelte",
|
|
26
|
+
"./components/Container": "./src/components/Container.svelte",
|
|
27
|
+
"./components/Disclosure": "./src/components/Disclosure.svelte",
|
|
28
|
+
"./components/Field": "./src/components/Field.svelte",
|
|
29
|
+
"./components/Figure": "./src/components/Figure.svelte",
|
|
30
|
+
"./components/Grid": "./src/components/Grid.svelte",
|
|
31
|
+
"./components/Input": "./src/components/Input.svelte",
|
|
32
|
+
"./components/Label": "./src/components/Label.svelte",
|
|
33
|
+
"./components/Select": "./src/components/Select.svelte",
|
|
34
|
+
"./components/Table": "./src/components/Table.svelte",
|
|
35
|
+
"./components/Textarea": "./src/components/Textarea.svelte",
|
|
36
|
+
"./components/Typography": "./src/components/Typography.svelte"
|
|
22
37
|
},
|
|
23
38
|
"files": [
|
|
24
39
|
"dist",
|
|
40
|
+
"src/components",
|
|
25
41
|
"README.md"
|
|
26
42
|
],
|
|
27
43
|
"publishConfig": {
|
|
28
44
|
"access": "public"
|
|
29
45
|
},
|
|
30
46
|
"dependencies": {
|
|
31
|
-
"@labcat2020/rocketship": "0.
|
|
47
|
+
"@labcat2020/rocketship": "0.2.0"
|
|
32
48
|
},
|
|
33
49
|
"devDependencies": {
|
|
50
|
+
"@sveltejs/vite-plugin-svelte": "^7.3.1",
|
|
51
|
+
"axe-core": "4.11.1",
|
|
52
|
+
"jsdom": "^30.1.2",
|
|
34
53
|
"sass": "^1.105.1",
|
|
35
54
|
"svelte": "^5.57.2",
|
|
36
55
|
"svelte-check": "^4.7.6",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte'
|
|
3
|
+
import type { Snippet } from 'svelte'
|
|
4
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
5
|
+
import { setAccordion, type AccordionItem } from './Disclosure.svelte'
|
|
6
|
+
|
|
7
|
+
export type AccordionMode = 'multiple' | 'single'
|
|
8
|
+
export type AccordionTag = 'div' | 'section' | 'article' | 'ul'
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
as?: AccordionTag
|
|
12
|
+
mode?: AccordionMode
|
|
13
|
+
class?: string
|
|
14
|
+
children?: Snippet
|
|
15
|
+
} & HTMLAttributes<HTMLElement>
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
as = 'div',
|
|
19
|
+
mode = 'multiple',
|
|
20
|
+
class: className = '',
|
|
21
|
+
children,
|
|
22
|
+
...attrs
|
|
23
|
+
}: Props = $props()
|
|
24
|
+
|
|
25
|
+
const items = new Set<AccordionItem>()
|
|
26
|
+
let lastOpened: AccordionItem | undefined
|
|
27
|
+
|
|
28
|
+
const closeSiblings = (item: AccordionItem) => {
|
|
29
|
+
if (mode !== 'single') return
|
|
30
|
+
for (const other of items) {
|
|
31
|
+
if (other !== item) other.close()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
setAccordion({
|
|
36
|
+
register: (item) => {
|
|
37
|
+
items.add(item)
|
|
38
|
+
},
|
|
39
|
+
unregister: (item) => {
|
|
40
|
+
items.delete(item)
|
|
41
|
+
if (lastOpened === item) lastOpened = undefined
|
|
42
|
+
},
|
|
43
|
+
opened: (item) => {
|
|
44
|
+
lastOpened = item
|
|
45
|
+
closeSiblings(item)
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Switching to single mode at runtime must reconcile panels that are already open,
|
|
50
|
+
// keeping the most recently opened one. `untrack` avoids subscribing to child state.
|
|
51
|
+
$effect(() => {
|
|
52
|
+
if (mode !== 'single') return
|
|
53
|
+
untrack(() => {
|
|
54
|
+
const open = [...items].filter((item) => item.isOpen())
|
|
55
|
+
if (open.length <= 1) return
|
|
56
|
+
const keep = lastOpened && open.includes(lastOpened) ? lastOpened : open[open.length - 1]
|
|
57
|
+
for (const item of open) {
|
|
58
|
+
if (item !== keep) item.close()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const classes = $derived(
|
|
64
|
+
['rs-accordion', mode === 'single' ? 'rs-accordion--single' : undefined, className]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(' '),
|
|
67
|
+
)
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<svelte:element this={as} {...attrs} class={classes}>
|
|
71
|
+
{@render children?.()}
|
|
72
|
+
</svelte:element>
|
|
73
|
+
|
|
74
|
+
<style lang="scss">
|
|
75
|
+
@use '@labcat2020/rocketship/styles/mixins' as *;
|
|
76
|
+
|
|
77
|
+
:global {
|
|
78
|
+
.rs-accordion {
|
|
79
|
+
--rs-accordion-gap: var(--rs-space-2, 8px);
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: var(--rs-accordion-gap);
|
|
83
|
+
|
|
84
|
+
&--single {
|
|
85
|
+
--rs-accordion-gap: 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
4
|
+
|
|
5
|
+
export type AlertVariant = 'info' | 'success' | 'warning' | 'error'
|
|
6
|
+
export type AlertTag = 'div' | 'section' | 'aside' | 'article'
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
as?: AlertTag
|
|
10
|
+
variant?: AlertVariant
|
|
11
|
+
title?: string
|
|
12
|
+
role?: 'alert' | 'status'
|
|
13
|
+
class?: string
|
|
14
|
+
titleSlot?: Snippet
|
|
15
|
+
children?: Snippet
|
|
16
|
+
} & HTMLAttributes<HTMLElement>
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
as = 'div',
|
|
20
|
+
variant = 'info',
|
|
21
|
+
title,
|
|
22
|
+
role: roleProp,
|
|
23
|
+
class: className = '',
|
|
24
|
+
titleSlot,
|
|
25
|
+
children,
|
|
26
|
+
...attrs
|
|
27
|
+
}: Props = $props()
|
|
28
|
+
|
|
29
|
+
const role = $derived(roleProp ?? (variant === 'error' || variant === 'warning' ? 'alert' : 'status'))
|
|
30
|
+
|
|
31
|
+
const classes = $derived(['rs-alert', `rs-alert--${variant}`, className].filter(Boolean).join(' '))
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<svelte:element this={as} {...attrs} class={classes} role={role}>
|
|
35
|
+
{#if title}
|
|
36
|
+
<h4 class="rs-alert__title">{title}</h4>
|
|
37
|
+
{:else if titleSlot}
|
|
38
|
+
<h4 class="rs-alert__title">
|
|
39
|
+
{@render titleSlot()}
|
|
40
|
+
</h4>
|
|
41
|
+
{/if}
|
|
42
|
+
<div class="rs-alert__body">
|
|
43
|
+
{@render children?.()}
|
|
44
|
+
</div>
|
|
45
|
+
</svelte:element>
|
|
46
|
+
|
|
47
|
+
<style lang="scss">
|
|
48
|
+
@use '@labcat2020/rocketship/styles/mixins' as *;
|
|
49
|
+
|
|
50
|
+
:global {
|
|
51
|
+
.rs-alert {
|
|
52
|
+
--rs-alert-bg: var(--rs-color-surface-elevated, #eef2ff);
|
|
53
|
+
--rs-alert-color: var(--rs-color-fg, #131a2f);
|
|
54
|
+
--rs-alert-border-color: var(--rs-color-border, #c8d2eb);
|
|
55
|
+
--rs-alert-accent-color: var(--rs-color-accent, #2f5bdb);
|
|
56
|
+
display: flex;
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
gap: var(--rs-alert-gap, var(--rs-space-2, 8px));
|
|
59
|
+
font-family: var(
|
|
60
|
+
--rs-font-family-sans,
|
|
61
|
+
'Inter',
|
|
62
|
+
'Segoe UI',
|
|
63
|
+
Roboto,
|
|
64
|
+
Helvetica,
|
|
65
|
+
Arial,
|
|
66
|
+
sans-serif
|
|
67
|
+
);
|
|
68
|
+
background: var(--rs-alert-bg);
|
|
69
|
+
border: var(--rs-alert-border-width, var(--rs-border-width-sm, 1px)) solid
|
|
70
|
+
var(--rs-alert-border-color);
|
|
71
|
+
border-inline-start: var(--rs-alert-accent-width, var(--rs-border-width-lg, 4px)) solid
|
|
72
|
+
var(--rs-alert-accent-color);
|
|
73
|
+
border-radius: var(--rs-alert-border-radius, var(--rs-radius-md, 10px));
|
|
74
|
+
color: var(--rs-alert-color);
|
|
75
|
+
padding-block: var(--rs-alert-padding-block, var(--rs-space-3, 12px));
|
|
76
|
+
padding-inline: var(--rs-alert-padding-inline, var(--rs-space-4, 16px));
|
|
77
|
+
|
|
78
|
+
@include container-query-up($rs-cq-sm) {
|
|
79
|
+
gap: var(--rs-alert-gap, var(--rs-space-3, 12px));
|
|
80
|
+
padding-block: var(--rs-alert-padding-block, var(--rs-space-4, 16px));
|
|
81
|
+
padding-inline: var(--rs-alert-padding-inline, var(--rs-space-5, 20px));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&--success {
|
|
85
|
+
--rs-alert-bg: var(--rs-color-success-bg, #e6f4ea);
|
|
86
|
+
--rs-alert-border-color: var(--rs-color-success-border, #a7d8b5);
|
|
87
|
+
--rs-alert-accent-color: var(--rs-color-success-accent, #1a7a32);
|
|
88
|
+
--rs-alert-color: var(--rs-color-success-fg, #0f2a18);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
&--warning {
|
|
92
|
+
--rs-alert-bg: var(--rs-color-warning-bg, #fff8e1);
|
|
93
|
+
--rs-alert-border-color: var(--rs-color-warning-border, #f0c36a);
|
|
94
|
+
--rs-alert-accent-color: var(--rs-color-warning-accent, #9a5d00);
|
|
95
|
+
--rs-alert-color: var(--rs-color-warning-fg, #3d2800);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&--error {
|
|
99
|
+
--rs-alert-bg: var(--rs-color-danger-bg, #ffeef0);
|
|
100
|
+
--rs-alert-border-color: var(--rs-color-danger-border, #f0a8b0);
|
|
101
|
+
--rs-alert-accent-color: var(--rs-color-danger-accent, #c5221f);
|
|
102
|
+
--rs-alert-color: var(--rs-color-danger-fg, #3d0a08);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
&__title {
|
|
106
|
+
color: var(--rs-alert-color);
|
|
107
|
+
font-size: var(--rs-alert-title-font-size, var(--rs-font-size-md, #{to-rem(16)}));
|
|
108
|
+
font-weight: var(--rs-alert-title-font-weight, var(--rs-font-weight-semibold, 600));
|
|
109
|
+
line-height: var(--rs-alert-title-line-height, var(--rs-line-height-tight, 1.2));
|
|
110
|
+
margin: 0;
|
|
111
|
+
|
|
112
|
+
@include container-query-up($rs-cq-sm) {
|
|
113
|
+
font-size: var(--rs-alert-title-font-size, var(--rs-font-size-lg, #{to-rem(18)}));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
&__body {
|
|
118
|
+
color: var(--rs-alert-color);
|
|
119
|
+
font-size: var(--rs-alert-body-font-size, var(--rs-font-size-sm, #{to-rem(14)}));
|
|
120
|
+
line-height: var(--rs-alert-body-line-height, var(--rs-line-height-base, 1.45));
|
|
121
|
+
margin: 0;
|
|
122
|
+
|
|
123
|
+
@include container-query-up($rs-cq-sm) {
|
|
124
|
+
font-size: var(--rs-alert-body-font-size, var(--rs-font-size-md, #{to-rem(16)}));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements'
|
|
4
|
+
|
|
5
|
+
export type ButtonVariant = 'primary' | 'secondary' | 'ghost'
|
|
6
|
+
export type ButtonSize = 'small' | 'medium' | 'large'
|
|
7
|
+
export type ButtonTag = 'button' | 'a'
|
|
8
|
+
|
|
9
|
+
type NativeButtonType = 'button' | 'submit' | 'reset'
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
as?: ButtonTag
|
|
13
|
+
href?: string
|
|
14
|
+
variant?: ButtonVariant
|
|
15
|
+
size?: ButtonSize
|
|
16
|
+
disabled?: boolean
|
|
17
|
+
type?: NativeButtonType
|
|
18
|
+
class?: string
|
|
19
|
+
children?: Snippet
|
|
20
|
+
} & HTMLButtonAttributes &
|
|
21
|
+
HTMLAnchorAttributes
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
as = 'button',
|
|
25
|
+
href,
|
|
26
|
+
variant = 'primary',
|
|
27
|
+
size = 'medium',
|
|
28
|
+
disabled = false,
|
|
29
|
+
type = 'button',
|
|
30
|
+
class: className = '',
|
|
31
|
+
children,
|
|
32
|
+
...attrs
|
|
33
|
+
}: Props = $props()
|
|
34
|
+
|
|
35
|
+
const classes = $derived(
|
|
36
|
+
['rs-button', `rs-button--${variant}`, `rs-button--${size}`, className].filter(Boolean).join(' '),
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const shouldRenderAsLink = $derived(as === 'a' || Boolean(href))
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
{#if shouldRenderAsLink}
|
|
43
|
+
<a
|
|
44
|
+
{...attrs}
|
|
45
|
+
aria-disabled={disabled || undefined}
|
|
46
|
+
class={classes}
|
|
47
|
+
href={disabled ? undefined : (href ?? '#')}
|
|
48
|
+
role={disabled ? 'link' : undefined}
|
|
49
|
+
tabindex={disabled ? -1 : undefined}
|
|
50
|
+
>
|
|
51
|
+
{@render children?.()}
|
|
52
|
+
</a>
|
|
53
|
+
{:else}
|
|
54
|
+
<button {...attrs} class={classes} disabled={disabled} type={type}>
|
|
55
|
+
{@render children?.()}
|
|
56
|
+
</button>
|
|
57
|
+
{/if}
|
|
58
|
+
|
|
59
|
+
<style lang="scss">
|
|
60
|
+
@use '@labcat2020/rocketship/styles/mixins' as *;
|
|
61
|
+
|
|
62
|
+
:global {
|
|
63
|
+
.rs-button {
|
|
64
|
+
all: unset;
|
|
65
|
+
--rs-button-bg: var(--rs-color-accent, #2f5bdb);
|
|
66
|
+
--rs-button-color: var(--rs-color-accent-contrast, #ffffff);
|
|
67
|
+
--rs-button-border-color: var(--rs-color-accent, #2f5bdb);
|
|
68
|
+
--rs-button-border-width: var(--rs-border-width, 1px);
|
|
69
|
+
--rs-button-border-radius: var(--rs-radius-pill, 999px);
|
|
70
|
+
--rs-button-font-size: var(--rs-font-size-sm, #{to-rem(14)});
|
|
71
|
+
--rs-button-font-weight: var(--rs-font-weight-semibold, 600);
|
|
72
|
+
--rs-button-line-height: var(--rs-line-height-tight, 1.2);
|
|
73
|
+
--rs-button-padding-inline: var(--rs-space-3, 12px);
|
|
74
|
+
--rs-button-min-height: 36px;
|
|
75
|
+
--rs-button-gap: var(--rs-space-2, 8px);
|
|
76
|
+
--rs-button-focus-ring-color: var(--rs-color-focus-ring, #296dff);
|
|
77
|
+
box-sizing: border-box;
|
|
78
|
+
align-items: center;
|
|
79
|
+
background: var(--rs-button-bg);
|
|
80
|
+
border: var(--rs-button-border-width) solid var(--rs-button-border-color);
|
|
81
|
+
border-radius: var(--rs-button-border-radius);
|
|
82
|
+
color: var(--rs-button-color);
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
display: inline-flex;
|
|
85
|
+
font-family: var(
|
|
86
|
+
--rs-font-family-sans,
|
|
87
|
+
'Inter',
|
|
88
|
+
'Segoe UI',
|
|
89
|
+
Roboto,
|
|
90
|
+
Helvetica,
|
|
91
|
+
Arial,
|
|
92
|
+
sans-serif
|
|
93
|
+
);
|
|
94
|
+
font-size: var(--rs-button-font-size);
|
|
95
|
+
font-weight: var(--rs-button-font-weight);
|
|
96
|
+
gap: var(--rs-button-gap);
|
|
97
|
+
justify-content: center;
|
|
98
|
+
line-height: var(--rs-button-line-height);
|
|
99
|
+
min-height: var(--rs-button-min-height);
|
|
100
|
+
padding-block: 8px;
|
|
101
|
+
padding-inline: var(--rs-button-padding-inline);
|
|
102
|
+
text-decoration: none;
|
|
103
|
+
transition:
|
|
104
|
+
background-color var(--rs-transition-fast, 140ms) ease,
|
|
105
|
+
border-color var(--rs-transition-fast, 140ms) ease,
|
|
106
|
+
color var(--rs-transition-fast, 140ms) ease,
|
|
107
|
+
opacity var(--rs-transition-fast, 140ms) ease;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
|
|
110
|
+
&:hover {
|
|
111
|
+
filter: brightness(1.08);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&:active {
|
|
115
|
+
filter: brightness(0.94);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&:focus-visible {
|
|
119
|
+
outline: 2px solid var(--rs-button-focus-ring-color);
|
|
120
|
+
outline-offset: 2px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
&:disabled,
|
|
124
|
+
&[aria-disabled='true'] {
|
|
125
|
+
cursor: not-allowed;
|
|
126
|
+
opacity: 0.55;
|
|
127
|
+
pointer-events: none;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&--secondary {
|
|
131
|
+
--rs-button-bg: var(--rs-color-surface, #ffffff);
|
|
132
|
+
--rs-button-border-color: var(--rs-color-accent, #2f5bdb);
|
|
133
|
+
--rs-button-border-width: var(--rs-border-width-md, 2px);
|
|
134
|
+
--rs-button-color: var(--rs-color-accent, #2f5bdb);
|
|
135
|
+
|
|
136
|
+
&:hover {
|
|
137
|
+
filter: none;
|
|
138
|
+
--rs-button-bg: var(--rs-color-surface-elevated, #eef2ff);
|
|
139
|
+
--rs-button-border-color: var(--rs-color-link-hover, #173ea8);
|
|
140
|
+
--rs-button-color: var(--rs-color-link-hover, #173ea8);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&--ghost {
|
|
145
|
+
--rs-button-bg: transparent;
|
|
146
|
+
--rs-button-border-color: transparent;
|
|
147
|
+
--rs-button-border-width: var(--rs-border-width-md, 2px);
|
|
148
|
+
--rs-button-color: var(--rs-color-accent, #2f5bdb);
|
|
149
|
+
|
|
150
|
+
&:hover {
|
|
151
|
+
filter: none;
|
|
152
|
+
--rs-button-bg: var(--rs-color-surface-elevated, #eef2ff);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
&--small {
|
|
157
|
+
--rs-button-font-size: var(--rs-button-font-size-sm);
|
|
158
|
+
--rs-button-min-height: var(--rs-button-min-height-sm);
|
|
159
|
+
--rs-button-padding-inline: var(--rs-button-padding-inline-sm);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
&--medium {
|
|
163
|
+
--rs-button-font-size: var(--rs-button-font-size-md);
|
|
164
|
+
--rs-button-min-height: var(--rs-button-min-height-md);
|
|
165
|
+
--rs-button-padding-inline: var(--rs-button-padding-inline-md);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
&--large {
|
|
169
|
+
--rs-button-font-size: var(--rs-button-font-size-lg);
|
|
170
|
+
--rs-button-min-height: var(--rs-button-min-height-lg);
|
|
171
|
+
--rs-button-padding-inline: var(--rs-button-padding-inline-lg);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@include container-query-up($rs-cq-sm) {
|
|
175
|
+
--rs-button-font-size-sm: var(--rs-button-font-size-sm-cq);
|
|
176
|
+
--rs-button-font-size-md: var(--rs-button-font-size-md-cq);
|
|
177
|
+
--rs-button-font-size-lg: var(--rs-button-font-size-lg-cq);
|
|
178
|
+
--rs-button-min-height-sm: var(--rs-button-min-height-sm-cq);
|
|
179
|
+
--rs-button-min-height-md: var(--rs-button-min-height-md-cq);
|
|
180
|
+
--rs-button-min-height-lg: var(--rs-button-min-height-lg-cq);
|
|
181
|
+
--rs-button-padding-inline-sm: var(--rs-button-padding-inline-sm-cq);
|
|
182
|
+
--rs-button-padding-inline-md: var(--rs-button-padding-inline-md-cq);
|
|
183
|
+
--rs-button-padding-inline-lg: var(--rs-button-padding-inline-lg-cq);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
</style>
|