@aiaiai-pt/design-system 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/components/Alert.svelte +100 -0
- package/components/Badge.svelte +108 -0
- package/components/BottomNav.svelte +37 -0
- package/components/BottomNavItem.svelte +121 -0
- package/components/Button.svelte +269 -0
- package/components/Card.svelte +108 -0
- package/components/Checkbox.svelte +138 -0
- package/components/CodeBlock.svelte +187 -0
- package/components/CodeEditor.svelte +221 -0
- package/components/CollapsibleSection.svelte +160 -0
- package/components/Combobox.svelte +396 -0
- package/components/EmptyState.svelte +148 -0
- package/components/FileUpload.svelte +280 -0
- package/components/FileUploadItem.svelte +222 -0
- package/components/Input.svelte +222 -0
- package/components/KeyValue.svelte +79 -0
- package/components/Label.svelte +49 -0
- package/components/List.svelte +70 -0
- package/components/ListItem.svelte +125 -0
- package/components/Menu.svelte +161 -0
- package/components/MenuItem.svelte +120 -0
- package/components/MenuSeparator.svelte +34 -0
- package/components/Modal.svelte +260 -0
- package/components/OptionGrid.svelte +195 -0
- package/components/Panel.svelte +256 -0
- package/components/Popover.svelte +194 -0
- package/components/Progress.svelte +78 -0
- package/components/Select.svelte +182 -0
- package/components/Separator.svelte +47 -0
- package/components/Sidebar.svelte +106 -0
- package/components/SidebarItem.svelte +154 -0
- package/components/SidebarSection.svelte +43 -0
- package/components/Skeleton.svelte +79 -0
- package/components/Status.svelte +104 -0
- package/components/Stepper.svelte +142 -0
- package/components/Tab.svelte +94 -0
- package/components/TabList.svelte +36 -0
- package/components/TabPanel.svelte +45 -0
- package/components/Tabs.svelte +46 -0
- package/components/Tag.svelte +96 -0
- package/components/Textarea.svelte +143 -0
- package/components/Toast.svelte +114 -0
- package/components/Toggle.svelte +132 -0
- package/components/index.js +70 -0
- package/package.json +45 -0
- package/tokens/base.css +175 -0
- package/tokens/components.css +530 -0
- package/tokens/semantic.css +211 -0
- package/tokens/themes/aiaiai.css +53 -0
- package/tokens/themes/bespoke-example.css +148 -0
- package/tokens/themes/branded-example.css +55 -0
- package/tokens/utilities.css +1865 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Card
|
|
3
|
+
|
|
4
|
+
Surface container. Three variants: flat, bordered (default), elevated.
|
|
5
|
+
Content-agnostic. Consumes --card-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
@example Bordered (default)
|
|
8
|
+
<Card>Content here</Card>
|
|
9
|
+
|
|
10
|
+
@example Elevated
|
|
11
|
+
<Card variant="elevated">Floating content</Card>
|
|
12
|
+
|
|
13
|
+
@example Interactive + selectable
|
|
14
|
+
<Card interactive selected={isSelected} onclick={() => select(id)}>
|
|
15
|
+
Clickable card
|
|
16
|
+
</Card>
|
|
17
|
+
-->
|
|
18
|
+
<script>
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {'flat' | 'bordered' | 'elevated'} Variant
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
let {
|
|
24
|
+
/** @type {Variant} */
|
|
25
|
+
variant = 'bordered',
|
|
26
|
+
/** @type {boolean} */
|
|
27
|
+
interactive = false,
|
|
28
|
+
/** @type {boolean} */
|
|
29
|
+
selected = false,
|
|
30
|
+
/** @type {string} */
|
|
31
|
+
class: className = '',
|
|
32
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
33
|
+
children = undefined,
|
|
34
|
+
...rest
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
// tag variable unused — rendering uses {#if interactive} branching
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if interactive}
|
|
41
|
+
<button
|
|
42
|
+
class="card card-{variant} card-interactive {className}"
|
|
43
|
+
class:card-selected={selected}
|
|
44
|
+
{...rest}
|
|
45
|
+
>
|
|
46
|
+
{#if children}{@render children()}{/if}
|
|
47
|
+
</button>
|
|
48
|
+
{:else}
|
|
49
|
+
<div
|
|
50
|
+
class="card card-{variant} {className}"
|
|
51
|
+
class:card-selected={selected}
|
|
52
|
+
{...rest}
|
|
53
|
+
>
|
|
54
|
+
{#if children}{@render children()}{/if}
|
|
55
|
+
</div>
|
|
56
|
+
{/if}
|
|
57
|
+
|
|
58
|
+
<style>
|
|
59
|
+
.card {
|
|
60
|
+
border-radius: var(--card-radius);
|
|
61
|
+
padding: var(--card-padding);
|
|
62
|
+
background: var(--card-bg);
|
|
63
|
+
transition: all var(--card-transition);
|
|
64
|
+
text-align: left;
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.card-flat {
|
|
70
|
+
border: var(--card-flat-border);
|
|
71
|
+
box-shadow: var(--card-flat-shadow);
|
|
72
|
+
background: var(--color-surface-secondary);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.card-bordered {
|
|
76
|
+
border: var(--card-bordered-border);
|
|
77
|
+
box-shadow: var(--card-bordered-shadow);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.card-elevated {
|
|
81
|
+
border: var(--card-elevated-border);
|
|
82
|
+
box-shadow: var(--card-elevated-shadow);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.card-interactive {
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
font-family: inherit;
|
|
88
|
+
font-size: inherit;
|
|
89
|
+
width: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.card-interactive:hover {
|
|
93
|
+
border: var(--card-interactive-hover-border);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.card-interactive:active {
|
|
97
|
+
background: var(--card-interactive-active-bg);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.card-interactive:focus-visible {
|
|
101
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
102
|
+
outline-offset: var(--focus-ring-offset);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.card-selected {
|
|
106
|
+
border-color: var(--card-selected-border-color);
|
|
107
|
+
}
|
|
108
|
+
</style>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Checkbox
|
|
3
|
+
|
|
4
|
+
Checkbox with label. Supports checked, indeterminate, and disabled states.
|
|
5
|
+
Consumes --checkbox-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
@example
|
|
8
|
+
<Checkbox label="Accept terms" bind:checked />
|
|
9
|
+
|
|
10
|
+
@example Indeterminate
|
|
11
|
+
<Checkbox label="Select all" indeterminate />
|
|
12
|
+
-->
|
|
13
|
+
<script module>
|
|
14
|
+
let _checkboxUid = 0;
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script>
|
|
18
|
+
let {
|
|
19
|
+
/** @type {boolean} */
|
|
20
|
+
checked = $bindable(false),
|
|
21
|
+
/** @type {boolean} */
|
|
22
|
+
indeterminate = false,
|
|
23
|
+
/** @type {boolean} */
|
|
24
|
+
disabled = false,
|
|
25
|
+
/** @type {string | undefined} */
|
|
26
|
+
label = undefined,
|
|
27
|
+
/** @type {string | undefined} */
|
|
28
|
+
id = undefined,
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
class: className = '',
|
|
31
|
+
...rest
|
|
32
|
+
} = $props();
|
|
33
|
+
|
|
34
|
+
const fallbackId = `checkbox-${_checkboxUid++}`;
|
|
35
|
+
const checkboxId = $derived(id ?? fallbackId);
|
|
36
|
+
|
|
37
|
+
/** @type {HTMLInputElement | undefined} */
|
|
38
|
+
let inputEl;
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
if (inputEl) {
|
|
42
|
+
inputEl.indeterminate = indeterminate;
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<label
|
|
48
|
+
class="checkbox-group {className}"
|
|
49
|
+
class:checkbox-group-disabled={disabled}
|
|
50
|
+
for={checkboxId}
|
|
51
|
+
>
|
|
52
|
+
<span
|
|
53
|
+
class="checkbox"
|
|
54
|
+
class:checkbox-checked={checked || indeterminate}
|
|
55
|
+
>
|
|
56
|
+
{#if checked}
|
|
57
|
+
<svg class="checkbox-icon" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
58
|
+
<path d="M2.5 6l2.5 2.5 4.5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
59
|
+
</svg>
|
|
60
|
+
{:else if indeterminate}
|
|
61
|
+
<svg class="checkbox-icon" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
62
|
+
<path d="M3 6h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
|
|
63
|
+
</svg>
|
|
64
|
+
{/if}
|
|
65
|
+
</span>
|
|
66
|
+
<input
|
|
67
|
+
bind:this={inputEl}
|
|
68
|
+
id={checkboxId}
|
|
69
|
+
type="checkbox"
|
|
70
|
+
class="checkbox-input"
|
|
71
|
+
{disabled}
|
|
72
|
+
bind:checked
|
|
73
|
+
{...rest}
|
|
74
|
+
/>
|
|
75
|
+
{#if label}
|
|
76
|
+
<span class="checkbox-label">{label}</span>
|
|
77
|
+
{/if}
|
|
78
|
+
</label>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.checkbox-group {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--space-sm);
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.checkbox-group-disabled {
|
|
89
|
+
opacity: 0.5;
|
|
90
|
+
cursor: not-allowed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.checkbox-input {
|
|
94
|
+
position: absolute;
|
|
95
|
+
opacity: 0;
|
|
96
|
+
width: 0;
|
|
97
|
+
height: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.checkbox {
|
|
101
|
+
width: var(--checkbox-size);
|
|
102
|
+
height: var(--checkbox-size);
|
|
103
|
+
border-radius: var(--checkbox-radius);
|
|
104
|
+
border: var(--checkbox-border);
|
|
105
|
+
background: var(--color-surface);
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
flex-shrink: 0;
|
|
110
|
+
transition: all var(--duration-fast) var(--easing-default);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.checkbox-group:has(.checkbox-input:focus-visible) .checkbox {
|
|
114
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
115
|
+
outline-offset: var(--focus-ring-offset);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.checkbox-checked {
|
|
119
|
+
background: var(--checkbox-bg-checked);
|
|
120
|
+
border-color: var(--checkbox-bg-checked);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.checkbox-icon {
|
|
124
|
+
width: 12px;
|
|
125
|
+
height: 12px;
|
|
126
|
+
color: var(--checkbox-check-color);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.checkbox-label {
|
|
130
|
+
font-family: var(--type-body-sm-font);
|
|
131
|
+
font-size: var(--type-body-sm-size);
|
|
132
|
+
color: var(--color-text);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.checkbox-group-disabled .checkbox-label {
|
|
136
|
+
color: var(--color-text-muted);
|
|
137
|
+
}
|
|
138
|
+
</style>
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CodeBlock
|
|
3
|
+
|
|
4
|
+
Formatted code display with optional line numbers.
|
|
5
|
+
Consumes --code-* tokens from components.css.
|
|
6
|
+
|
|
7
|
+
Syntax highlighting is the consumer's responsibility (Prism, Shiki, etc).
|
|
8
|
+
This component handles layout, line numbers, and copy.
|
|
9
|
+
|
|
10
|
+
@example
|
|
11
|
+
<CodeBlock code="SELECT * FROM users WHERE active = true;" language="sql" />
|
|
12
|
+
|
|
13
|
+
@example Without line numbers
|
|
14
|
+
<CodeBlock code="pip install pandas" lineNumbers={false} />
|
|
15
|
+
|
|
16
|
+
@example With highlighted HTML (pre-highlighted by consumer)
|
|
17
|
+
<CodeBlock language="python" lineNumbers>
|
|
18
|
+
{#snippet content()}
|
|
19
|
+
<span class="token keyword">def</span> <span class="token function">main</span>():
|
|
20
|
+
{/snippet}
|
|
21
|
+
</CodeBlock>
|
|
22
|
+
-->
|
|
23
|
+
<script>
|
|
24
|
+
let {
|
|
25
|
+
/** @type {string | undefined} */
|
|
26
|
+
code = undefined,
|
|
27
|
+
/** @type {string | undefined} */
|
|
28
|
+
language = undefined,
|
|
29
|
+
/** @type {boolean} */
|
|
30
|
+
lineNumbers = true,
|
|
31
|
+
/** @type {boolean} */
|
|
32
|
+
copyable = true,
|
|
33
|
+
/** @type {string} */
|
|
34
|
+
class: className = '',
|
|
35
|
+
/** @type {import('svelte').Snippet | undefined} */
|
|
36
|
+
content = undefined,
|
|
37
|
+
...rest
|
|
38
|
+
} = $props();
|
|
39
|
+
|
|
40
|
+
let copied = $state(false);
|
|
41
|
+
|
|
42
|
+
const lines = $derived(code?.split('\n') ?? []);
|
|
43
|
+
|
|
44
|
+
async function handleCopy() {
|
|
45
|
+
if (!code) return;
|
|
46
|
+
try {
|
|
47
|
+
await navigator.clipboard.writeText(code);
|
|
48
|
+
copied = true;
|
|
49
|
+
setTimeout(() => { copied = false; }, 2000);
|
|
50
|
+
} catch {
|
|
51
|
+
// Clipboard API not available
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<div
|
|
57
|
+
class="code-block {className}"
|
|
58
|
+
data-language={language}
|
|
59
|
+
{...rest}
|
|
60
|
+
>
|
|
61
|
+
{#if copyable && code}
|
|
62
|
+
<button
|
|
63
|
+
class="code-copy"
|
|
64
|
+
onclick={handleCopy}
|
|
65
|
+
aria-label={copied ? 'Copied' : 'Copy code'}
|
|
66
|
+
>
|
|
67
|
+
<span class="code-copy-label">
|
|
68
|
+
{copied ? 'COPIED' : 'COPY'}
|
|
69
|
+
</span>
|
|
70
|
+
</button>
|
|
71
|
+
{/if}
|
|
72
|
+
|
|
73
|
+
<div class="code-scroll">
|
|
74
|
+
{#if content}
|
|
75
|
+
<!-- Pre-highlighted content via snippet -->
|
|
76
|
+
<pre class="code-pre"><code>{@render content()}</code></pre>
|
|
77
|
+
{:else if code}
|
|
78
|
+
<!-- Plain text with optional line numbers -->
|
|
79
|
+
<table class="code-table" role="presentation">
|
|
80
|
+
<tbody>
|
|
81
|
+
{#each lines as line, i}
|
|
82
|
+
<tr class="code-line">
|
|
83
|
+
{#if lineNumbers}
|
|
84
|
+
<td class="code-line-number" aria-hidden="true">{i + 1}</td>
|
|
85
|
+
{/if}
|
|
86
|
+
<td class="code-line-content">{line || '\u00A0'}</td>
|
|
87
|
+
</tr>
|
|
88
|
+
{/each}
|
|
89
|
+
</tbody>
|
|
90
|
+
</table>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<style>
|
|
96
|
+
.code-block {
|
|
97
|
+
position: relative;
|
|
98
|
+
background: var(--code-bg);
|
|
99
|
+
border: var(--code-border);
|
|
100
|
+
border-radius: var(--code-radius);
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.code-scroll {
|
|
105
|
+
overflow-x: auto;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.code-pre {
|
|
109
|
+
margin: 0;
|
|
110
|
+
padding: var(--code-padding);
|
|
111
|
+
font-family: var(--code-font);
|
|
112
|
+
font-size: var(--code-font-size);
|
|
113
|
+
line-height: var(--code-line-height);
|
|
114
|
+
color: var(--code-text);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.code-table {
|
|
118
|
+
border-collapse: collapse;
|
|
119
|
+
width: 100%;
|
|
120
|
+
font-family: var(--code-font);
|
|
121
|
+
font-size: var(--code-font-size);
|
|
122
|
+
line-height: var(--code-line-height);
|
|
123
|
+
color: var(--code-text);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.code-line {
|
|
127
|
+
display: block;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.code-line-number {
|
|
131
|
+
width: var(--code-line-number-width);
|
|
132
|
+
padding: 0 var(--space-sm);
|
|
133
|
+
text-align: right;
|
|
134
|
+
color: var(--code-line-number-color);
|
|
135
|
+
background: var(--code-gutter-bg);
|
|
136
|
+
border-right: var(--code-gutter-border);
|
|
137
|
+
user-select: none;
|
|
138
|
+
vertical-align: top;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.code-line-content {
|
|
142
|
+
padding: 0 var(--code-padding);
|
|
143
|
+
white-space: pre;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* First and last rows get vertical padding */
|
|
147
|
+
.code-table tbody tr:first-child .code-line-number,
|
|
148
|
+
.code-table tbody tr:first-child .code-line-content {
|
|
149
|
+
padding-top: var(--code-padding);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.code-table tbody tr:last-child .code-line-number,
|
|
153
|
+
.code-table tbody tr:last-child .code-line-content {
|
|
154
|
+
padding-bottom: var(--code-padding);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ─── Copy button ─── */
|
|
158
|
+
.code-copy {
|
|
159
|
+
all: unset;
|
|
160
|
+
position: absolute;
|
|
161
|
+
top: var(--space-sm);
|
|
162
|
+
right: var(--space-sm);
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
z-index: 1;
|
|
165
|
+
padding: var(--space-2xs) var(--space-sm);
|
|
166
|
+
border-radius: var(--radius-sm);
|
|
167
|
+
background: var(--code-bg);
|
|
168
|
+
border: var(--code-border);
|
|
169
|
+
transition: all var(--duration-instant) var(--easing-default);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.code-copy:hover {
|
|
173
|
+
background: var(--color-surface-tertiary);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.code-copy:focus-visible {
|
|
177
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
178
|
+
outline-offset: var(--focus-ring-offset);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.code-copy-label {
|
|
182
|
+
font-family: var(--type-label-font);
|
|
183
|
+
font-size: var(--type-caption-size);
|
|
184
|
+
letter-spacing: var(--type-label-tracking);
|
|
185
|
+
color: var(--color-text-secondary);
|
|
186
|
+
}
|
|
187
|
+
</style>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component CodeEditor
|
|
3
|
+
|
|
4
|
+
Editable code editor wrapping CodeMirror 6.
|
|
5
|
+
Styled from --code-* tokens to match CodeBlock appearance.
|
|
6
|
+
Requires @codemirror/* peer dependencies installed by the consumer.
|
|
7
|
+
|
|
8
|
+
@example SQL editor
|
|
9
|
+
<CodeEditor bind:value={sql} language="sql" />
|
|
10
|
+
|
|
11
|
+
@example Readonly Python
|
|
12
|
+
<CodeEditor value={code} readonly language="python" minLines={10} />
|
|
13
|
+
|
|
14
|
+
@example Empty with placeholder
|
|
15
|
+
<CodeEditor language="sql" placeholder="Enter your query..." />
|
|
16
|
+
-->
|
|
17
|
+
<script>
|
|
18
|
+
import { onMount } from 'svelte';
|
|
19
|
+
import { EditorView, keymap, placeholder as phExtension, lineNumbers as lnExtension } from '@codemirror/view';
|
|
20
|
+
import { EditorState } from '@codemirror/state';
|
|
21
|
+
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
|
22
|
+
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
23
|
+
import { sql as sqlLang } from '@codemirror/lang-sql';
|
|
24
|
+
import { python as pythonLang } from '@codemirror/lang-python';
|
|
25
|
+
import { json as jsonLang } from '@codemirror/lang-json';
|
|
26
|
+
|
|
27
|
+
/** @type {{ value?: string, language?: string, readonly?: boolean, placeholder?: string, lineNumbers?: boolean, minLines?: number, maxLines?: number, class?: string }} */
|
|
28
|
+
let {
|
|
29
|
+
/** @type {string} */
|
|
30
|
+
value = $bindable(''),
|
|
31
|
+
/** @type {string} */
|
|
32
|
+
language = 'sql',
|
|
33
|
+
/** @type {boolean} */
|
|
34
|
+
readonly = false,
|
|
35
|
+
/** @type {string} */
|
|
36
|
+
placeholder = '',
|
|
37
|
+
/** @type {boolean} */
|
|
38
|
+
lineNumbers = true,
|
|
39
|
+
/** @type {number} */
|
|
40
|
+
minLines = 5,
|
|
41
|
+
/** @type {number} */
|
|
42
|
+
maxLines = 20,
|
|
43
|
+
/** @type {string} */
|
|
44
|
+
class: className = '',
|
|
45
|
+
} = $props();
|
|
46
|
+
|
|
47
|
+
/** @type {HTMLDivElement} */
|
|
48
|
+
let container;
|
|
49
|
+
/** @type {EditorView | null} */
|
|
50
|
+
let view = null;
|
|
51
|
+
let internalUpdate = false;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the language extension for the given language name.
|
|
55
|
+
* @param {string} lang
|
|
56
|
+
*/
|
|
57
|
+
function getLanguageExtension(lang) {
|
|
58
|
+
switch (lang) {
|
|
59
|
+
case 'sql': return sqlLang();
|
|
60
|
+
case 'python': return pythonLang();
|
|
61
|
+
case 'json': return jsonLang();
|
|
62
|
+
default: return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build a CM6 theme from CSS custom properties on the container element.
|
|
68
|
+
* @param {HTMLElement} el
|
|
69
|
+
* @param {number} minH
|
|
70
|
+
* @param {number} maxH
|
|
71
|
+
*/
|
|
72
|
+
function buildThemeFromTokens(el, minH, maxH) {
|
|
73
|
+
const s = getComputedStyle(el);
|
|
74
|
+
const get = (/** @type {string} */ prop) => s.getPropertyValue(prop).trim();
|
|
75
|
+
|
|
76
|
+
const bg = get('--code-bg') || 'inherit';
|
|
77
|
+
const text = get('--code-text') || 'inherit';
|
|
78
|
+
const font = get('--code-font') || 'monospace';
|
|
79
|
+
const fontSize = get('--code-font-size') || '14px';
|
|
80
|
+
const lineHeight = get('--code-line-height') || '1.6';
|
|
81
|
+
const gutterBg = get('--code-gutter-bg') || 'inherit';
|
|
82
|
+
const gutterBorder = get('--code-gutter-border') || 'var(--elevation-border)';
|
|
83
|
+
const lineNumColor = get('--code-line-number-color') || 'inherit';
|
|
84
|
+
const selectionBg = get('--code-selection-bg') || 'rgba(0,0,0,0.1)';
|
|
85
|
+
const cursorColor = get('--code-cursor-color') || 'currentColor';
|
|
86
|
+
|
|
87
|
+
return EditorView.theme({
|
|
88
|
+
'&': {
|
|
89
|
+
backgroundColor: bg,
|
|
90
|
+
color: text,
|
|
91
|
+
fontFamily: font,
|
|
92
|
+
fontSize,
|
|
93
|
+
lineHeight,
|
|
94
|
+
minHeight: `calc(${lineHeight} * ${fontSize} * ${minH})`,
|
|
95
|
+
maxHeight: `calc(${lineHeight} * ${fontSize} * ${maxH})`,
|
|
96
|
+
},
|
|
97
|
+
'&.cm-focused': {
|
|
98
|
+
outline: 'none',
|
|
99
|
+
},
|
|
100
|
+
'.cm-scroller': {
|
|
101
|
+
overflow: 'auto',
|
|
102
|
+
fontFamily: font,
|
|
103
|
+
},
|
|
104
|
+
'.cm-content': {
|
|
105
|
+
caretColor: cursorColor,
|
|
106
|
+
fontFamily: font,
|
|
107
|
+
padding: '0',
|
|
108
|
+
},
|
|
109
|
+
'.cm-cursor, .cm-dropCursor': {
|
|
110
|
+
borderLeftColor: cursorColor,
|
|
111
|
+
borderLeftWidth: '2px',
|
|
112
|
+
},
|
|
113
|
+
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground': {
|
|
114
|
+
backgroundColor: selectionBg,
|
|
115
|
+
},
|
|
116
|
+
'.cm-gutters': {
|
|
117
|
+
backgroundColor: gutterBg,
|
|
118
|
+
borderRight: gutterBorder || '1px solid #ddd',
|
|
119
|
+
color: lineNumColor,
|
|
120
|
+
},
|
|
121
|
+
'.cm-lineNumbers .cm-gutterElement': {
|
|
122
|
+
padding: '0 8px 0 4px',
|
|
123
|
+
minWidth: '32px',
|
|
124
|
+
},
|
|
125
|
+
'.cm-activeLine': {
|
|
126
|
+
backgroundColor: 'transparent',
|
|
127
|
+
},
|
|
128
|
+
'.cm-activeLineGutter': {
|
|
129
|
+
backgroundColor: 'transparent',
|
|
130
|
+
},
|
|
131
|
+
'.cm-placeholder': {
|
|
132
|
+
color: lineNumColor,
|
|
133
|
+
fontStyle: 'italic',
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
onMount(() => {
|
|
139
|
+
const extensions = [
|
|
140
|
+
history(),
|
|
141
|
+
keymap.of([...defaultKeymap, ...historyKeymap]),
|
|
142
|
+
syntaxHighlighting(defaultHighlightStyle),
|
|
143
|
+
buildThemeFromTokens(container, minLines, maxLines),
|
|
144
|
+
EditorView.updateListener.of((/** @type {import('@codemirror/view').ViewUpdate} */ update) => {
|
|
145
|
+
if (update.docChanged) {
|
|
146
|
+
internalUpdate = true;
|
|
147
|
+
value = update.state.doc.toString();
|
|
148
|
+
internalUpdate = false;
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
if (lineNumbers) {
|
|
154
|
+
extensions.push(lnExtension());
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (placeholder) {
|
|
158
|
+
extensions.push(phExtension(placeholder));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (readonly) {
|
|
162
|
+
extensions.push(EditorState.readOnly.of(true));
|
|
163
|
+
extensions.push(EditorView.editable.of(false));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const langExt = getLanguageExtension(language);
|
|
167
|
+
if (langExt) {
|
|
168
|
+
extensions.push(langExt);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
view = new EditorView({
|
|
172
|
+
state: EditorState.create({ doc: /** @type {string} */ (value), extensions }),
|
|
173
|
+
parent: container,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return () => {
|
|
177
|
+
view?.destroy();
|
|
178
|
+
view = null;
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// Sync external value changes into the editor
|
|
183
|
+
$effect(() => {
|
|
184
|
+
if (view && !internalUpdate) {
|
|
185
|
+
const current = view.state.doc.toString();
|
|
186
|
+
if (value !== current) {
|
|
187
|
+
view.dispatch({
|
|
188
|
+
changes: { from: 0, to: current.length, insert: /** @type {string} */ (value) },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
</script>
|
|
194
|
+
|
|
195
|
+
<div
|
|
196
|
+
class="code-editor {className}"
|
|
197
|
+
class:code-editor--readonly={readonly}
|
|
198
|
+
bind:this={container}
|
|
199
|
+
data-language={language}
|
|
200
|
+
></div>
|
|
201
|
+
|
|
202
|
+
<style>
|
|
203
|
+
.code-editor {
|
|
204
|
+
border: var(--code-border);
|
|
205
|
+
border-radius: var(--code-radius);
|
|
206
|
+
overflow: hidden;
|
|
207
|
+
background: var(--code-bg);
|
|
208
|
+
transition: box-shadow var(--duration-instant) var(--easing-default);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.code-editor:focus-within:not(.code-editor--readonly) {
|
|
212
|
+
box-shadow:
|
|
213
|
+
0 0 0 var(--focus-ring-offset) var(--color-surface),
|
|
214
|
+
0 0 0 calc(var(--focus-ring-offset) + var(--focus-ring-width)) var(--focus-ring-color);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* CM6 places its own wrapper — let it fill the container */
|
|
218
|
+
.code-editor :global(.cm-editor) {
|
|
219
|
+
border-radius: var(--code-radius);
|
|
220
|
+
}
|
|
221
|
+
</style>
|