Skip to content

Commit 5b92f54

Browse files
committed
Added theming support
1 parent 435a415 commit 5b92f54

File tree

8 files changed

+199
-50
lines changed

8 files changed

+199
-50
lines changed

src/lib/components/CommandPalette.svelte

Lines changed: 90 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
11
<script lang="ts">
2-
import { paletteStore } from '../store/PaletteStore';
32
import { onDestroy, onMount, afterUpdate } from 'svelte';
4-
// import tinyKeys, { parseKeybinding } from 'tinykeys';
3+
import { setContext as setThemeContext } from 'svelte';
4+
import { paletteStore } from '../store/PaletteStore';
55
import Portal from './Portal.svelte';
66
import ResultPanel from './ResultPanel.svelte';
77
import KeyboardButton from './KeyboardButton.svelte';
88
import createShortcuts from '../utils/createShortcuts';
9-
import { createFuse, formatResults, getNonEmptyArray, runAction } from '../utils';
9+
import { createFuse, formatResults, getNonEmptyArray, runAction, toCssString } from '../utils';
1010
import createStoreMethods from '../utils/createStoreMethods';
1111
import createActionMap from '../utils/createActionMap';
12-
import type { ActionId, commands, storeParams } from '$lib/types';
12+
import { THEME_CONTEXT } from '../constants';
13+
import type { ActionId, commands, storeParams, className, cssStyle } from '$lib/types';
14+
import type { Properties } from 'csstype';
1315
1416
export let commands: commands = [];
17+
export let placeholder: string = 'Search for an action';
18+
19+
// style classes
20+
21+
export let inputClass: className = null;
22+
export let overlayClass: className = null;
23+
export let paletteWrapperInnerClass: className = null;
24+
export let resultsContainerClass: className = null;
25+
export let resultContainerClass: className = null;
26+
export let optionSelectedClass: className = null;
27+
export let titleClass: className = null;
28+
export let subtitleClass: className = null;
29+
export let descriptionClass: className = null;
30+
export let keyboardButtonClass: className = null;
31+
export let unstyled = false;
32+
33+
// style props (convert to css)
34+
35+
export let inputStyle: Properties = {};
36+
export let overlayStyle: Properties = {};
37+
export let paletteWrapperInnerStyle: Properties = {};
38+
export let resultsContainerStyle: Properties = {};
39+
export let resultContainerStyle: Properties = {};
40+
export let optionSelectedStyle: Properties = {};
41+
export let titleStyle: Properties = {};
42+
export let subtitleStyle: Properties = {};
43+
export let descriptionStyle: Properties = {};
44+
export let keyboardButtonStyle: Properties = {};
45+
1546
let wrapperElement: HTMLDivElement;
1647
let searchInputRef: HTMLInputElement;
1748
let commandPaletteRef: HTMLElement;
@@ -25,6 +56,32 @@
2556
2657
let actions: commands = [];
2758
59+
// set themes to context to pass down to deeply nested components
60+
61+
setThemeContext(THEME_CONTEXT, {
62+
inputClass,
63+
overlayClass,
64+
paletteWrapperInnerClass,
65+
resultsContainerClass,
66+
resultContainerClass,
67+
optionSelectedClass,
68+
titleClass,
69+
subtitleClass,
70+
descriptionClass,
71+
keyboardButtonClass,
72+
unstyled,
73+
inputStyle: toCssString(inputStyle),
74+
overlayStyle: toCssString(overlayStyle),
75+
paletteWrapperInnerStyle: toCssString(paletteWrapperInnerStyle),
76+
resultsContainerStyle: toCssString(resultsContainerStyle),
77+
resultContainerStyle: toCssString(resultContainerStyle),
78+
optionSelectedStyle: toCssString(optionSelectedStyle),
79+
titleStyle: toCssString(titleStyle),
80+
subtitleStyle: toCssString(subtitleStyle),
81+
descriptionStyle: toCssString(descriptionStyle),
82+
keyboardButtonStyle: toCssString(keyboardButtonStyle)
83+
});
84+
2885
const storeMethods = createStoreMethods();
2986
const actionMap = createActionMap(commands);
3087
let formattedEscKey = '';
@@ -54,7 +111,8 @@
54111
lastActiveElement?.focus();
55112
};
56113
57-
const closePalette = () => {
114+
const closePalette = (event?: KeyboardEvent) => {
115+
event?.preventDefault?.();
58116
closeCommandPalette();
59117
focusLastElement();
60118
};
@@ -170,20 +228,34 @@
170228

171229
<Portal target="body">
172230
{#if isPaletteVisible}
173-
<div id="wrapper" bind:this={wrapperElement}>
231+
<div
232+
id="wrapper"
233+
class={overlayClass}
234+
style={toCssString(overlayStyle)}
235+
class:wrapper={!unstyled}
236+
bind:this={wrapperElement}
237+
>
174238
<div
175239
class="paletteWrapper"
176240
role="combobox"
177241
aria-expanded={true}
178242
aria-haspopup="listbox"
179243
aria-controls={'uniqId'}
180244
>
181-
<div class="paletteWrapperInner" bind:this={commandPaletteRef}>
245+
<div
246+
class:paletteWrapperInner={!unstyled}
247+
class={paletteWrapperInnerClass}
248+
style={toCssString(paletteWrapperInnerStyle)}
249+
bind:this={commandPaletteRef}
250+
>
182251
<form autocomplete="off" role="search" novalidate on:submit={(ev) => ev.preventDefault()}>
183252
<label for={searchInputId}>Search for an action</label>
184253
<input
185254
type="search"
186-
placeholder="Search for an action"
255+
class={inputClass}
256+
class:paletteInput={!unstyled}
257+
style={toCssString(inputStyle)}
258+
{placeholder}
187259
aria-autocomplete="list"
188260
spellcheck={false}
189261
aria-activedescendant={`palette-${activeCommand}`}
@@ -195,7 +267,7 @@
195267
on:input={handleSearch}
196268
/>
197269
<div class="shortcut">
198-
<KeyboardButton on:KeyboardButtonClicked={closePalette}
270+
<KeyboardButton on:KeyboardButtonClicked={() => closePalette()}
199271
>{formattedEscKey}</KeyboardButton
200272
>
201273
</div>
@@ -208,7 +280,7 @@
208280
</Portal>
209281

210282
<style>
211-
#wrapper {
283+
.wrapper {
212284
position: fixed;
213285
top: 0;
214286
left: 0;
@@ -221,8 +293,6 @@
221293
}
222294
223295
:global(.paletteWrapper *) {
224-
padding: 0;
225-
margin: 0;
226296
box-sizing: border-box;
227297
}
228298
@@ -247,24 +317,24 @@
247317
width: 100%;
248318
}
249319
250-
input {
320+
.paletteInput {
251321
width: 100%;
252322
padding: 1rem;
253323
appearance: none;
254324
border: none;
255325
}
256326
257-
input::placeholder {
327+
.paletteInput::placeholder {
258328
font-size: 20px;
259329
}
260330
261-
input[type='search']::-webkit-search-decoration,
262-
input[type='search']::-webkit-search-cancel-button,
263-
input[type='search']::-webkit-search-results-button,
264-
input[type='search']::-webkit-search-results-decoration {
331+
.paletteInput[type='search']::-webkit-search-decoration,
332+
.paletteInput[type='search']::-webkit-search-cancel-button,
333+
.paletteInput[type='search']::-webkit-search-results-button,
334+
.paletteInput[type='search']::-webkit-search-results-decoration {
265335
-webkit-appearance: none;
266336
}
267-
input:focus {
337+
.paletteInput:focus {
268338
outline: none;
269339
}
270340
@@ -286,7 +356,7 @@
286356
height: 100vh;
287357
max-height: 100vh;
288358
}
289-
#wrapper {
359+
.wrapper {
290360
padding: 0;
291361
}
292362
}

src/lib/components/KeyboardButton.svelte

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
<script lang="ts">
2-
import { createEventDispatcher } from 'svelte';
2+
import { THEME_CONTEXT } from '$lib/constants';
3+
import type { themeContext } from '$lib/types';
4+
import { createEventDispatcher, getContext } from 'svelte';
35
const dispatch = createEventDispatcher();
46
57
const handleButtonClick = (event: MouseEvent) => {
68
dispatch('KeyboardButtonClicked', {
79
event
810
});
911
};
12+
13+
const themeContext: themeContext = getContext(THEME_CONTEXT);
14+
const { unstyled, keyboardButtonClass, keyboardButtonStyle } = themeContext;
1015
</script>
1116

12-
<button on:click={handleButtonClick}>
17+
<button
18+
style={keyboardButtonStyle}
19+
class:keyboardButton={!unstyled}
20+
class={keyboardButtonClass}
21+
on:click={handleButtonClick}
22+
>
1323
<slot />
1424
</button>
1525

1626
<style>
17-
button {
27+
.keyboardButton {
1828
background: #cbd5e0;
1929
padding: 0.25rem 0.5rem;
2030
box-shadow: 0 0 0 0px #fff, 0, 0 1px 2px rgb(0 0 0/0.05) 0 0 #0000;

src/lib/components/Result.svelte

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
<script lang="ts">
22
import { paletteStore } from '../store/PaletteStore';
3-
import { afterUpdate, onMount } from 'svelte';
3+
import { afterUpdate, onMount, getContext } from 'svelte';
44
import { runAction } from '../utils';
55
import KeyboardButton from './KeyboardButton.svelte';
6-
import type { action } from '$lib/types';
6+
import { THEME_CONTEXT } from '../constants';
7+
import type { action, themeContext } from '$lib/types';
78
89
export let action: action;
910
let elRef: HTMLElement;
1011
let isActive: boolean;
1112
let formattedShortcut: Array<string[] | string> = [];
1213
14+
const themeContext: themeContext = getContext(THEME_CONTEXT);
15+
const {
16+
resultContainerClass,
17+
unstyled,
18+
optionSelectedClass,
19+
titleClass,
20+
subtitleClass,
21+
descriptionClass,
22+
resultContainerStyle,
23+
titleStyle,
24+
subtitleStyle,
25+
descriptionStyle,
26+
optionSelectedStyle
27+
} = themeContext;
28+
1329
afterUpdate(() => {
1430
if (action.actionId === $paletteStore.activeCommandId && elRef) {
1531
isActive = true;
1632
requestAnimationFrame(() => {
17-
elRef.scrollIntoView({
33+
elRef?.scrollIntoView?.({
1834
behavior: 'smooth',
1935
block: 'nearest'
2036
});
@@ -32,14 +48,12 @@
3248
3349
onMount(async () => {
3450
const tinyKeys = await import('tinykeys');
35-
const {parseKeybinding} = tinyKeys
51+
const { parseKeybinding } = tinyKeys;
3652
if (action.shortcut) {
37-
const parsedShortcut = parseKeybinding(action.shortcut);
38-
formattedShortcut = parsedShortcut.flat().filter((s) => s.length > 0);
39-
}
40-
})
41-
42-
53+
const parsedShortcut = parseKeybinding(action.shortcut);
54+
formattedShortcut = parsedShortcut.flat().filter((s) => s.length > 0);
55+
}
56+
});
4357
4458
const onMouseEnter = () => {
4559
isActive = true;
@@ -58,18 +72,24 @@
5872
</script>
5973

6074
<li
75+
class:resultContainer={!unstyled}
76+
class={`${resultContainerClass} ${
77+
isActive ? (!unstyled ? 'selected' : optionSelectedClass) : ''
78+
}`}
79+
style={`${resultContainerStyle} ${isActive ? optionSelectedStyle : ''}`}
6180
aria-selected={isActive}
6281
role="option"
6382
bind:this={elRef}
6483
on:click={handleRunAction}
65-
class:selected={isActive}
6684
on:mouseenter={onMouseEnter}
6785
on:mouseleave={onMouseLeave}
6886
>
6987
<div>
70-
<h4 class="title">{action.title}</h4>
71-
<p class="subtitle">{action.subTitle}</p>
72-
<p class="description">{action.description || ''}</p>
88+
<h4 style={titleStyle} class:title={!unstyled} class={titleClass}>{action.title}</h4>
89+
<p style={subtitleStyle} class:subtitle={!unstyled} class={subtitleClass}>{action.subTitle}</p>
90+
<p style={descriptionStyle} class:description={!unstyled} class={descriptionClass}>
91+
{action.description || ''}
92+
</p>
7393
</div>
7494
<div class="shortcuts">
7595
{#each formattedShortcut as shortcut}
@@ -83,7 +103,7 @@
83103
</li>
84104

85105
<style>
86-
li {
106+
.resultContainer {
87107
padding: 1rem;
88108
border-bottom: 1px solid #f7fafc;
89109
display: flex;

src/lib/components/ResultPanel.svelte

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
<script lang="ts">
22
import { paletteStore } from '../store/PaletteStore';
3-
import { onDestroy } from 'svelte';
3+
import { onDestroy, getContext } from 'svelte';
44
import Result from './Result.svelte';
55
import { getNonEmptyArray } from '../utils';
6-
import type { commands, storeParams } from '$lib/types';
6+
import { THEME_CONTEXT } from '../constants';
7+
import type { commands, storeParams, themeContext } from '$lib/types';
78
89
let actions: commands = [];
910
const unsubscribe = paletteStore.subscribe((value: storeParams) => {
1011
actions = getNonEmptyArray(value.results);
1112
});
1213
14+
const themeContext: themeContext = getContext(THEME_CONTEXT);
15+
const { resultsContainerClass, unstyled, resultsContainerStyle } = themeContext;
16+
1317
onDestroy(unsubscribe);
1418
</script>
1519

1620
{#if actions.length > 0}
17-
<ul role="listbox">
21+
<ul
22+
class={resultsContainerClass}
23+
class:results={!unstyled}
24+
style={resultsContainerStyle}
25+
role="listbox"
26+
>
1827
{#each actions as action (action.actionId)}
1928
<Result {action} />
2029
{/each}
@@ -26,7 +35,7 @@
2635
{/if}
2736

2837
<style>
29-
ul {
38+
.results {
3039
width: 100%;
3140
list-style-type: none;
3241
background: white;

src/lib/constants/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,6 @@ const defaultAppState = {
77
results: []
88
};
99

10-
export { defaultAppState };
10+
const THEME_CONTEXT = 'themeContext';
11+
12+
export { defaultAppState, THEME_CONTEXT };

0 commit comments

Comments
 (0)