figma-plugin-utilities 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.
- package/README.md +223 -0
- package/package.json +53 -0
- package/src/components/EmptyState.svelte +116 -0
- package/src/components/FieldGroup.svelte +34 -0
- package/src/components/Footer.svelte +110 -0
- package/src/components/Header.svelte +81 -0
- package/src/components/ListItem.svelte +172 -0
- package/src/components/LoadingState.svelte +32 -0
- package/src/components/PluginLayout.svelte +43 -0
- package/src/components/StatusBar.svelte +106 -0
- package/src/components/index.js +9 -0
- package/src/index.js +45 -0
- package/src/lib/colors.js +77 -0
- package/src/lib/errorHandling.js +186 -0
- package/src/lib/figma-helpers.ts +129 -0
- package/src/lib/index.js +47 -0
- package/src/lib/messages.js +36 -0
- package/src/lib/resize.js +147 -0
- package/src/lib/validation.js +234 -0
package/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# figma-plugin-utils
|
|
2
|
+
|
|
3
|
+
Shared Svelte components and utilities for Figma plugins.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install figma-plugin-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Import Everything
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import {
|
|
17
|
+
// Components
|
|
18
|
+
PluginLayout,
|
|
19
|
+
Header,
|
|
20
|
+
Footer,
|
|
21
|
+
StatusBar,
|
|
22
|
+
EmptyState,
|
|
23
|
+
ListItem,
|
|
24
|
+
LoadingState,
|
|
25
|
+
FieldGroup,
|
|
26
|
+
// Utilities
|
|
27
|
+
sendToPlugin,
|
|
28
|
+
createMessageHandler,
|
|
29
|
+
rgbToHex,
|
|
30
|
+
hexToRgb,
|
|
31
|
+
validateUrl,
|
|
32
|
+
validateJsonString,
|
|
33
|
+
sanitizeName,
|
|
34
|
+
} from "figma-plugin-utils";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Import Specific Modules
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
// Components only
|
|
41
|
+
import { PluginLayout, Header, Footer } from "figma-plugin-utils/components";
|
|
42
|
+
|
|
43
|
+
// Utilities only
|
|
44
|
+
import { sendToPlugin, createMessageHandler } from "figma-plugin-utils/lib";
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
| Component | Description |
|
|
50
|
+
|-----------|-------------|
|
|
51
|
+
| `PluginLayout` | Main content wrapper with scrollable area |
|
|
52
|
+
| `Header` | Header bar with `left`, `center`, `right` slots and optional title |
|
|
53
|
+
| `Footer` | Footer with `right`, `split`, and `full` layout variants |
|
|
54
|
+
| `StatusBar` | Toast notifications with auto-dismiss (info/success/error/warning) |
|
|
55
|
+
| `EmptyState` | Empty/error states with optional icon and action buttons |
|
|
56
|
+
| `ListItem` | Selectable list items with metadata slot and action menu |
|
|
57
|
+
| `LoadingState` | Centered loading indicator with custom message |
|
|
58
|
+
| `FieldGroup` | Label + input wrapper for form fields |
|
|
59
|
+
|
|
60
|
+
### Header
|
|
61
|
+
|
|
62
|
+
```svelte
|
|
63
|
+
<Header title="My Plugin">
|
|
64
|
+
<svelte:fragment slot="left">
|
|
65
|
+
<IconButton iconName={IconBack} on:click={goBack} />
|
|
66
|
+
</svelte:fragment>
|
|
67
|
+
<svelte:fragment slot="right">
|
|
68
|
+
<IconButton iconName={IconSettings} />
|
|
69
|
+
</svelte:fragment>
|
|
70
|
+
</Header>
|
|
71
|
+
|
|
72
|
+
<!-- Without border -->
|
|
73
|
+
<Header title="Settings" noBorder />
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Footer
|
|
77
|
+
|
|
78
|
+
```svelte
|
|
79
|
+
<!-- Right-aligned (default) -->
|
|
80
|
+
<Footer>
|
|
81
|
+
<Button variant="primary">Save</Button>
|
|
82
|
+
</Footer>
|
|
83
|
+
|
|
84
|
+
<!-- Split layout -->
|
|
85
|
+
<Footer variant="split">
|
|
86
|
+
<svelte:fragment slot="left">
|
|
87
|
+
<Button variant="secondary">Cancel</Button>
|
|
88
|
+
</svelte:fragment>
|
|
89
|
+
<svelte:fragment slot="right">
|
|
90
|
+
<Button variant="primary">Save</Button>
|
|
91
|
+
</svelte:fragment>
|
|
92
|
+
</Footer>
|
|
93
|
+
|
|
94
|
+
<!-- Full-width buttons -->
|
|
95
|
+
<Footer variant="full">
|
|
96
|
+
<Button variant="primary">Generate</Button>
|
|
97
|
+
</Footer>
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### StatusBar
|
|
101
|
+
|
|
102
|
+
```svelte
|
|
103
|
+
<StatusBar
|
|
104
|
+
message={status.message}
|
|
105
|
+
type={status.type}
|
|
106
|
+
on:close={() => status = { message: '', type: 'info' }}
|
|
107
|
+
/>
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Types: `info`, `success`, `error`, `warning`. Auto-dismisses after 4s for `info` and `success`.
|
|
111
|
+
|
|
112
|
+
### EmptyState
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<EmptyState
|
|
116
|
+
message="No items yet"
|
|
117
|
+
icon="search"
|
|
118
|
+
actions={[
|
|
119
|
+
{ label: "Add Item", handler: handleAdd },
|
|
120
|
+
{ label: "Import", handler: handleImport }
|
|
121
|
+
]}
|
|
122
|
+
/>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### ListItem
|
|
126
|
+
|
|
127
|
+
```svelte
|
|
128
|
+
<ListItem
|
|
129
|
+
id="item-1"
|
|
130
|
+
title="My Item"
|
|
131
|
+
active={selectedId === 'item-1'}
|
|
132
|
+
menuItems={[
|
|
133
|
+
{ label: 'Edit', value: 'edit' },
|
|
134
|
+
{ label: 'Delete', value: 'delete' }
|
|
135
|
+
]}
|
|
136
|
+
on:click={handleSelect}
|
|
137
|
+
on:menuSelect={handleMenuAction}
|
|
138
|
+
>
|
|
139
|
+
<span>Additional metadata</span>
|
|
140
|
+
</ListItem>
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Utilities
|
|
144
|
+
|
|
145
|
+
### Messages (`lib/messages.js`)
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
// Send message to plugin code
|
|
149
|
+
sendToPlugin("my-action", { data: "value" });
|
|
150
|
+
|
|
151
|
+
// Handle messages from plugin
|
|
152
|
+
window.onmessage = createMessageHandler({
|
|
153
|
+
success: (msg) => console.log("Success:", msg),
|
|
154
|
+
error: (msg) => console.error("Error:", msg),
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Colors (`lib/colors.js`)
|
|
159
|
+
|
|
160
|
+
```javascript
|
|
161
|
+
// Convert between formats (Figma uses 0-1 range)
|
|
162
|
+
const rgb = hexToRgb("#FF0000"); // { r: 1, g: 0, b: 0 }
|
|
163
|
+
const hex = rgbToHex({ r: 1, g: 0, b: 0 }); // "#FF0000"
|
|
164
|
+
|
|
165
|
+
// Calculate contrast
|
|
166
|
+
const ratio = getContrastRatio(color1, color2);
|
|
167
|
+
const passes = meetsContrastLevel(ratio, "AA"); // true/false
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Validation (`lib/validation.js`)
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
const urlResult = validateUrl("https://example.com");
|
|
174
|
+
// { valid: true } or { valid: false, error: "..." }
|
|
175
|
+
|
|
176
|
+
const jsonResult = validateJsonString('{"key": "value"}');
|
|
177
|
+
// { valid: true, parsed: {...} } or { valid: false, error: "..." }
|
|
178
|
+
|
|
179
|
+
const clean = sanitizeName("My Plugin!!!"); // "My Plugin"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Error Handling (`lib/errorHandling.js`)
|
|
183
|
+
|
|
184
|
+
```javascript
|
|
185
|
+
// Safe async operations
|
|
186
|
+
const result = await safeAsync(
|
|
187
|
+
() => fetch(url),
|
|
188
|
+
"Loading data"
|
|
189
|
+
);
|
|
190
|
+
if (result.ok) {
|
|
191
|
+
console.log(result.value);
|
|
192
|
+
} else {
|
|
193
|
+
console.error(result.error.userMessage);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Parse JSON safely
|
|
197
|
+
const parsed = parseJsonSafe(jsonString);
|
|
198
|
+
// { ok: true, value: {...} } or { ok: false, error: "..." }
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Figma Helpers (`lib/figma-helpers.ts`)
|
|
202
|
+
|
|
203
|
+
For use in `code.ts`:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { sendToUI, showError, focusNodes, loadFont } from "figma-plugin-utils/lib/figma-helpers";
|
|
207
|
+
|
|
208
|
+
// Send message to UI
|
|
209
|
+
sendToUI("success", { message: "Done!" });
|
|
210
|
+
|
|
211
|
+
// Show notification
|
|
212
|
+
showError("Something went wrong");
|
|
213
|
+
|
|
214
|
+
// Focus viewport on nodes
|
|
215
|
+
focusNodes(figma.currentPage.selection);
|
|
216
|
+
|
|
217
|
+
// Load font before using
|
|
218
|
+
await loadFont("Inter", "Regular");
|
|
219
|
+
|
|
220
|
+
// Client storage
|
|
221
|
+
await saveToStorage("settings", { theme: "dark" });
|
|
222
|
+
const settings = await loadFromStorage("settings", { theme: "light" });
|
|
223
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "figma-plugin-utilities",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared Svelte components and utilities for Figma plugins",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"svelte": "./src/index.js",
|
|
7
|
+
"main": "./src/index.js",
|
|
8
|
+
"module": "./src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"svelte": "./src/index.js",
|
|
12
|
+
"import": "./src/index.js",
|
|
13
|
+
"default": "./src/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./components": {
|
|
16
|
+
"svelte": "./src/components/index.js",
|
|
17
|
+
"import": "./src/components/index.js",
|
|
18
|
+
"default": "./src/components/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./lib": {
|
|
21
|
+
"import": "./src/lib/index.js",
|
|
22
|
+
"default": "./src/lib/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./lib/figma-helpers": {
|
|
25
|
+
"types": "./src/lib/figma-helpers.ts",
|
|
26
|
+
"import": "./src/lib/figma-helpers.ts",
|
|
27
|
+
"default": "./src/lib/figma-helpers.ts"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": ["src"],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/mariusroosendaal/figma-plugin-utilities.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/mariusroosendaal/figma-plugin-utilities/issues"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/mariusroosendaal/figma-plugin-utilities#readme",
|
|
39
|
+
"keywords": [
|
|
40
|
+
"figma",
|
|
41
|
+
"plugin",
|
|
42
|
+
"svelte",
|
|
43
|
+
"components",
|
|
44
|
+
"utilities",
|
|
45
|
+
"figma-plugin"
|
|
46
|
+
],
|
|
47
|
+
"author": "Marius Roosendaal",
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"figma-ui3-kit-svelte": "^0.4.0",
|
|
51
|
+
"svelte": "^4.2.20"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Button, Icon } from "figma-ui3-kit-svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Empty state display with optional icon and action buttons
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <EmptyState
|
|
9
|
+
* message="No items found"
|
|
10
|
+
* icon="search"
|
|
11
|
+
* actions={[{ label: "Add Item", handler: handleAdd }]}
|
|
12
|
+
* />
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Message to display */
|
|
16
|
+
export let message = "";
|
|
17
|
+
|
|
18
|
+
/** Optional icon (string name or component) */
|
|
19
|
+
export let icon = null;
|
|
20
|
+
|
|
21
|
+
/** Single action for backward compatibility { label, handler } */
|
|
22
|
+
export let action = null;
|
|
23
|
+
|
|
24
|
+
/** Multiple actions [{ label, handler }] */
|
|
25
|
+
export let actions = null;
|
|
26
|
+
|
|
27
|
+
/** Size variant: 'small', 'medium', 'large' */
|
|
28
|
+
export let size = "medium";
|
|
29
|
+
|
|
30
|
+
/** Whether to center vertically */
|
|
31
|
+
export let centered = true;
|
|
32
|
+
|
|
33
|
+
// Normalize actions
|
|
34
|
+
$: normalizedActions = actions ? actions : action ? [action] : null;
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<div
|
|
38
|
+
class="empty-state"
|
|
39
|
+
class:centered
|
|
40
|
+
class:small={size === "small"}
|
|
41
|
+
class:large={size === "large"}
|
|
42
|
+
>
|
|
43
|
+
{#if icon}
|
|
44
|
+
<div class="empty-state__icon">
|
|
45
|
+
{#if typeof icon === "string"}
|
|
46
|
+
<Icon iconName={icon} />
|
|
47
|
+
{:else}
|
|
48
|
+
<svelte:component this={icon} />
|
|
49
|
+
{/if}
|
|
50
|
+
</div>
|
|
51
|
+
{/if}
|
|
52
|
+
|
|
53
|
+
<div class="empty-state__message">
|
|
54
|
+
{message}
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
{#if normalizedActions && normalizedActions.length > 0}
|
|
58
|
+
<div class="empty-state__actions">
|
|
59
|
+
{#each normalizedActions as actionItem}
|
|
60
|
+
<Button variant="secondary" on:click={actionItem.handler}>
|
|
61
|
+
{actionItem.label}
|
|
62
|
+
</Button>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
{/if}
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<style>
|
|
69
|
+
.empty-state {
|
|
70
|
+
text-align: center;
|
|
71
|
+
padding: var(--size-xsmall);
|
|
72
|
+
color: var(--figma-color-text-secondary);
|
|
73
|
+
display: flex;
|
|
74
|
+
flex-direction: column;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: var(--size-xsmall);
|
|
77
|
+
font-family: var(--font-stack);
|
|
78
|
+
text-wrap: balance;
|
|
79
|
+
flex: 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.empty-state.centered {
|
|
83
|
+
justify-content: center;
|
|
84
|
+
align-items: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.empty-state.small {
|
|
88
|
+
font-size: var(--body-medium-font-size);
|
|
89
|
+
font-weight: var(--body-medium-font-weight);
|
|
90
|
+
letter-spacing: var(--body-medium-letter-spacing);
|
|
91
|
+
line-height: var(--body-medium-line-height);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.empty-state:not(.small):not(.large) {
|
|
95
|
+
font-size: var(--body-medium-font-size);
|
|
96
|
+
font-weight: var(--body-medium-font-weight);
|
|
97
|
+
letter-spacing: var(--body-medium-letter-spacing);
|
|
98
|
+
line-height: var(--body-medium-line-height);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.empty-state.large {
|
|
102
|
+
font-size: var(--body-large-font-size);
|
|
103
|
+
font-weight: var(--body-large-font-weight);
|
|
104
|
+
letter-spacing: var(--body-large-letter-spacing);
|
|
105
|
+
line-height: var(--body-large-line-height);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.empty-state__actions {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex-direction: row;
|
|
111
|
+
gap: var(--size-xxsmall);
|
|
112
|
+
align-items: center;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
}
|
|
116
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { Label } from "figma-ui3-kit-svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Field group wrapper
|
|
6
|
+
* Wraps a form field with an optional label
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* <FieldGroup label="Collection">
|
|
10
|
+
* <Dropdown menuItems={options} bind:value={selected} />
|
|
11
|
+
* </FieldGroup>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Label text (optional) */
|
|
15
|
+
export let label = "";
|
|
16
|
+
|
|
17
|
+
/** For attribute for the label (optional) */
|
|
18
|
+
export let labelFor = "";
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<div class="field-group">
|
|
22
|
+
{#if label}
|
|
23
|
+
<Label for={labelFor}>{label}</Label>
|
|
24
|
+
{/if}
|
|
25
|
+
<slot />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<style>
|
|
29
|
+
.field-group {
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
gap: var(--size-xxsmall);
|
|
33
|
+
}
|
|
34
|
+
</style>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
/**
|
|
3
|
+
* Plugin footer with layout variants
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* <!-- Right-aligned (default) -->
|
|
7
|
+
* <Footer>
|
|
8
|
+
* <Button>Save</Button>
|
|
9
|
+
* </Footer>
|
|
10
|
+
*
|
|
11
|
+
* <!-- Split layout -->
|
|
12
|
+
* <Footer variant="split">
|
|
13
|
+
* <svelte:fragment slot="left">
|
|
14
|
+
* <Button variant="secondary">Cancel</Button>
|
|
15
|
+
* </svelte:fragment>
|
|
16
|
+
* <svelte:fragment slot="right">
|
|
17
|
+
* <Button variant="primary">Save</Button>
|
|
18
|
+
* </svelte:fragment>
|
|
19
|
+
* </Footer>
|
|
20
|
+
*
|
|
21
|
+
* <!-- Full width buttons -->
|
|
22
|
+
* <Footer variant="full">
|
|
23
|
+
* <Button variant="primary">Create item</Button>
|
|
24
|
+
* </Footer>
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Layout variant: 'right', 'split', 'full' */
|
|
28
|
+
export let variant = "right";
|
|
29
|
+
|
|
30
|
+
/** Additional CSS class */
|
|
31
|
+
export let className = "";
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="footer footer--{variant} {className}">
|
|
35
|
+
{#if variant === "right"}
|
|
36
|
+
<div class="footer__right">
|
|
37
|
+
<slot />
|
|
38
|
+
</div>
|
|
39
|
+
{:else if variant === "split"}
|
|
40
|
+
<div class="footer__left">
|
|
41
|
+
<slot name="left" />
|
|
42
|
+
</div>
|
|
43
|
+
<div class="footer__right">
|
|
44
|
+
<slot name="right" />
|
|
45
|
+
</div>
|
|
46
|
+
{:else if variant === "full"}
|
|
47
|
+
<slot />
|
|
48
|
+
{/if}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.footer {
|
|
53
|
+
height: var(--size-large);
|
|
54
|
+
bottom: 0;
|
|
55
|
+
left: 0;
|
|
56
|
+
right: 0;
|
|
57
|
+
display: flex;
|
|
58
|
+
gap: var(--size-xxsmall);
|
|
59
|
+
padding: var(--size-xxsmall);
|
|
60
|
+
border-top: 1px solid var(--figma-color-border);
|
|
61
|
+
background: var(--figma-color-bg);
|
|
62
|
+
z-index: 10;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.footer :global(> *),
|
|
66
|
+
.footer :global(> * > *) {
|
|
67
|
+
display: flex;
|
|
68
|
+
gap: var(--size-xxsmall);
|
|
69
|
+
align-items: center;
|
|
70
|
+
min-width: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.footer--right {
|
|
74
|
+
justify-content: flex-end;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.footer--right .footer__right {
|
|
78
|
+
display: flex;
|
|
79
|
+
gap: var(--size-xxsmall);
|
|
80
|
+
align-items: center;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.footer--split {
|
|
84
|
+
justify-content: space-between;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.footer--split .footer__left {
|
|
88
|
+
display: flex;
|
|
89
|
+
gap: var(--size-xxsmall);
|
|
90
|
+
align-items: center;
|
|
91
|
+
flex: 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.footer--split .footer__right {
|
|
95
|
+
display: flex;
|
|
96
|
+
gap: var(--size-xxsmall);
|
|
97
|
+
align-items: center;
|
|
98
|
+
justify-content: end;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.footer--full {
|
|
102
|
+
display: flex;
|
|
103
|
+
width: 100%;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.footer--full :global(button) {
|
|
107
|
+
flex: 1;
|
|
108
|
+
width: 100%;
|
|
109
|
+
}
|
|
110
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// Header component with left/center/right slots
|
|
3
|
+
|
|
4
|
+
/** Additional CSS class */
|
|
5
|
+
export let className = "";
|
|
6
|
+
|
|
7
|
+
/** Title text (displayed in left section) */
|
|
8
|
+
export let title = "";
|
|
9
|
+
|
|
10
|
+
/** Remove bottom border */
|
|
11
|
+
export let noBorder = false;
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class="header {className}"
|
|
16
|
+
class:has-left-content={$$slots.left}
|
|
17
|
+
class:no-border={noBorder}
|
|
18
|
+
>
|
|
19
|
+
<div class="header__left">
|
|
20
|
+
<slot name="left" />
|
|
21
|
+
{#if title}
|
|
22
|
+
<h2 class="header__title">{title}</h2>
|
|
23
|
+
{/if}
|
|
24
|
+
</div>
|
|
25
|
+
<div class="header__center">
|
|
26
|
+
<slot name="center" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="header__right">
|
|
29
|
+
<slot name="right" />
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.header {
|
|
35
|
+
height: var(--size-large);
|
|
36
|
+
display: flex;
|
|
37
|
+
align-items: center;
|
|
38
|
+
justify-content: space-between;
|
|
39
|
+
padding: 0 var(--size-xxsmall) 0 var(--size-xsmall);
|
|
40
|
+
border-bottom: 1px solid var(--figma-color-border);
|
|
41
|
+
background: var(--figma-color-bg);
|
|
42
|
+
gap: var(--size-xsmall);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.header.has-left-content {
|
|
46
|
+
padding-left: var(--size-xxsmall);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.header.no-border {
|
|
50
|
+
border-bottom: none;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.header__left {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: var(--size-xxsmall);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.header__center {
|
|
60
|
+
flex: 1;
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: center;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.header__title {
|
|
67
|
+
font-size: var(--body-medium-font-size);
|
|
68
|
+
font-weight: var(--body-medium-font-weight);
|
|
69
|
+
letter-spacing: var(--body-medium-letter-spacing);
|
|
70
|
+
line-height: var(--body-medium-line-height);
|
|
71
|
+
color: var(--figma-color-text);
|
|
72
|
+
margin: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.header__right {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
gap: var(--size-xxsmall);
|
|
79
|
+
position: relative;
|
|
80
|
+
}
|
|
81
|
+
</style>
|