@reddb-io/ui-kit 0.1.1 → 0.2.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/package.json +1 -1
- package/src/lib/EmptyState.svelte +30 -0
- package/src/lib/ListRow.svelte +67 -0
- package/src/lib/LoadingState.svelte +20 -0
- package/src/lib/NavItem.svelte +53 -0
- package/src/lib/Pill.svelte +28 -0
- package/src/lib/SectionHeading.svelte +25 -0
- package/src/lib/SplitView.svelte +145 -0
- package/src/lib/index.ts +17 -5
- package/src/lib/split-view.ts +38 -0
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// The "live or empty — never fake" fallback: a centered icon + title +
|
|
3
|
+
// optional description, with an optional `action` slot (a fix command, a
|
|
4
|
+
// primary button). Used when a list has no rows or a surface is unreachable.
|
|
5
|
+
import type { Snippet } from 'svelte'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
title: string
|
|
9
|
+
description?: string
|
|
10
|
+
/** Leading icon, rendered muted above the title. */
|
|
11
|
+
icon?: Snippet
|
|
12
|
+
/** Optional call to action beneath the copy. */
|
|
13
|
+
action?: Snippet
|
|
14
|
+
class?: string
|
|
15
|
+
}
|
|
16
|
+
let { title, description, icon, action, class: klass = '' }: Props = $props()
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<div class="flex flex-col items-center justify-center gap-2 px-6 py-10 text-center {klass}">
|
|
20
|
+
{#if icon}
|
|
21
|
+
<span class="flex size-8 items-center justify-center text-fg-3">{@render icon()}</span>
|
|
22
|
+
{/if}
|
|
23
|
+
<p class="text-[13px] text-fg-1">{title}</p>
|
|
24
|
+
{#if description}
|
|
25
|
+
<p class="max-w-prose text-[12px] leading-relaxed text-fg-2">{description}</p>
|
|
26
|
+
{/if}
|
|
27
|
+
{#if action}
|
|
28
|
+
<div class="mt-2">{@render action()}</div>
|
|
29
|
+
{/if}
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A settings/governance row: a `title` (+ optional `description` and a mono
|
|
3
|
+
// `hint`) on the left, a right-aligned `action` slot, and an optional `below`
|
|
4
|
+
// slot that spans the row's full width (for inline editors, diffs, warnings).
|
|
5
|
+
//
|
|
6
|
+
// `wide` flips the layout to a stacked variant: the label block sits on top
|
|
7
|
+
// and the action drops to its own full-width line beneath — for actions that
|
|
8
|
+
// are too large to sit inline (a long input, a segmented control).
|
|
9
|
+
import type { Snippet } from 'svelte'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
title: string
|
|
13
|
+
/** Secondary line under the title. */
|
|
14
|
+
description?: string
|
|
15
|
+
/** Mono hint — a value, key, or id rendered in the data typeface. */
|
|
16
|
+
hint?: string
|
|
17
|
+
/** Right-aligned (inline) or full-width-below (wide) action. */
|
|
18
|
+
action?: Snippet
|
|
19
|
+
/** Full-width content beneath the row — editors, diffs, warnings. */
|
|
20
|
+
below?: Snippet
|
|
21
|
+
/** Stack the action beneath the label block instead of inline-right. */
|
|
22
|
+
wide?: boolean
|
|
23
|
+
class?: string
|
|
24
|
+
}
|
|
25
|
+
let {
|
|
26
|
+
title,
|
|
27
|
+
description,
|
|
28
|
+
hint,
|
|
29
|
+
action,
|
|
30
|
+
below,
|
|
31
|
+
wide = false,
|
|
32
|
+
class: klass = '',
|
|
33
|
+
}: Props = $props()
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
data-wide={wide ? '' : undefined}
|
|
38
|
+
class={[
|
|
39
|
+
'px-3 py-2.5',
|
|
40
|
+
wide ? 'flex flex-col gap-2' : 'flex items-center gap-3',
|
|
41
|
+
klass,
|
|
42
|
+
].join(' ')}
|
|
43
|
+
>
|
|
44
|
+
<div class={['flex min-w-0 items-baseline gap-2', wide ? '' : 'flex-1'].join(' ')}>
|
|
45
|
+
<div class="min-w-0">
|
|
46
|
+
<div class="flex items-baseline gap-2">
|
|
47
|
+
<span class="truncate text-[13px] text-fg-0">{title}</span>
|
|
48
|
+
{#if hint}
|
|
49
|
+
<span class="shrink-0 font-mono text-[11px] text-fg-2">{hint}</span>
|
|
50
|
+
{/if}
|
|
51
|
+
</div>
|
|
52
|
+
{#if description}
|
|
53
|
+
<p class="mt-0.5 text-[12px] leading-snug text-fg-2">{description}</p>
|
|
54
|
+
{/if}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
{#if action}
|
|
59
|
+
<div class={['flex items-center gap-2', wide ? 'w-full' : 'shrink-0'].join(' ')}>
|
|
60
|
+
{@render action()}
|
|
61
|
+
</div>
|
|
62
|
+
{/if}
|
|
63
|
+
|
|
64
|
+
{#if below}
|
|
65
|
+
<div class="w-full">{@render below()}</div>
|
|
66
|
+
{/if}
|
|
67
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A quiet inline loading affordance — a spinning ring + optional label.
|
|
3
|
+
// Honors `prefers-reduced-motion` (the spin is dropped when the user asks for
|
|
4
|
+
// reduced motion). Pair with a surface while its data is in flight.
|
|
5
|
+
interface Props {
|
|
6
|
+
label?: string
|
|
7
|
+
class?: string
|
|
8
|
+
}
|
|
9
|
+
let { label = 'Loading…', class: klass = '' }: Props = $props()
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="flex items-center justify-center gap-2 px-6 py-10 text-fg-2 {klass}">
|
|
13
|
+
<span
|
|
14
|
+
class="size-3.5 rounded-full border-2 border-line-2 border-t-fg-2 motion-safe:animate-spin"
|
|
15
|
+
aria-hidden="true"
|
|
16
|
+
></span>
|
|
17
|
+
{#if label}
|
|
18
|
+
<span class="text-[12px]">{label}</span>
|
|
19
|
+
{/if}
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A navigation entry for the SplitView nav pane: leading icon + label, an
|
|
3
|
+
// active state (accent-marked, `aria-current="page"`), and an optional
|
|
4
|
+
// trailing slot (a count Pill, a chevron). Renders as an `<a>` when `href` is
|
|
5
|
+
// given, otherwise a `<button>` — so it works for both routing and local
|
|
6
|
+
// section state.
|
|
7
|
+
import type { Snippet } from 'svelte'
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
label: string
|
|
11
|
+
icon?: Snippet
|
|
12
|
+
trailing?: Snippet
|
|
13
|
+
active?: boolean
|
|
14
|
+
href?: string
|
|
15
|
+
onclick?: (e: MouseEvent) => void
|
|
16
|
+
class?: string
|
|
17
|
+
}
|
|
18
|
+
let {
|
|
19
|
+
label,
|
|
20
|
+
icon,
|
|
21
|
+
trailing,
|
|
22
|
+
active = false,
|
|
23
|
+
href,
|
|
24
|
+
onclick,
|
|
25
|
+
class: klass = '',
|
|
26
|
+
}: Props = $props()
|
|
27
|
+
|
|
28
|
+
const cls = $derived(
|
|
29
|
+
[
|
|
30
|
+
'inline-flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-[13px] transition-colors',
|
|
31
|
+
active ? 'bg-bg-2 text-fg-0' : 'text-fg-2 hover:bg-bg-2 hover:text-fg-0',
|
|
32
|
+
klass,
|
|
33
|
+
].join(' '),
|
|
34
|
+
)
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
{#if href}
|
|
38
|
+
<a {href} {onclick} aria-current={active ? 'page' : undefined} class={cls}>
|
|
39
|
+
{#if icon}
|
|
40
|
+
<span class="flex size-3.5 shrink-0 items-center justify-center">{@render icon()}</span>
|
|
41
|
+
{/if}
|
|
42
|
+
<span class="flex-1 truncate">{label}</span>
|
|
43
|
+
{#if trailing}<span class="shrink-0">{@render trailing()}</span>{/if}
|
|
44
|
+
</a>
|
|
45
|
+
{:else}
|
|
46
|
+
<button type="button" {onclick} aria-current={active ? 'page' : undefined} class={cls}>
|
|
47
|
+
{#if icon}
|
|
48
|
+
<span class="flex size-3.5 shrink-0 items-center justify-center">{@render icon()}</span>
|
|
49
|
+
{/if}
|
|
50
|
+
<span class="flex-1 truncate">{label}</span>
|
|
51
|
+
{#if trailing}<span class="shrink-0">{@render trailing()}</span>{/if}
|
|
52
|
+
</button>
|
|
53
|
+
{/if}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { tv } from 'tailwind-variants'
|
|
3
|
+
// A small meta chip for counts/labels next to a heading — quieter than Badge
|
|
4
|
+
// (no uppercase/tracking), tuned for inline use beside a SectionHeading.
|
|
5
|
+
export const pill = tv({
|
|
6
|
+
base:
|
|
7
|
+
'inline-flex items-center gap-1 px-1.5 h-[18px] rounded-full text-[11px] ' +
|
|
8
|
+
'font-mono leading-none border',
|
|
9
|
+
variants: {
|
|
10
|
+
tone: {
|
|
11
|
+
muted: 'bg-bg-2 text-fg-2 border-line-2',
|
|
12
|
+
accent: 'bg-accent/15 text-accent border-accent/30',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: { tone: 'muted' },
|
|
16
|
+
})
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
interface Props {
|
|
21
|
+
tone?: 'muted' | 'accent'
|
|
22
|
+
class?: string
|
|
23
|
+
children?: import('svelte').Snippet
|
|
24
|
+
}
|
|
25
|
+
let { tone = 'muted', class: klass = '', children }: Props = $props()
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<span class={pill({ tone, class: klass })}>{@render children?.()}</span>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// A section title row: optional leading icon + title + optional trailing meta
|
|
3
|
+
// (typically a `Pill` count). Density-first — sits above a stack of ListRows.
|
|
4
|
+
import type { Snippet } from 'svelte'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
title: string
|
|
8
|
+
/** Leading icon (e.g. a lucide component rendered in a snippet). */
|
|
9
|
+
icon?: Snippet
|
|
10
|
+
/** Trailing meta — usually a `Pill` with a count. */
|
|
11
|
+
meta?: Snippet
|
|
12
|
+
class?: string
|
|
13
|
+
}
|
|
14
|
+
let { title, icon, meta, class: klass = '' }: Props = $props()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="flex items-center gap-2 {klass}">
|
|
18
|
+
{#if icon}
|
|
19
|
+
<span class="flex size-4 shrink-0 items-center justify-center text-fg-2">{@render icon()}</span>
|
|
20
|
+
{/if}
|
|
21
|
+
<h2 class="text-[11px] font-mono uppercase tracking-[0.1em] text-fg-2">{title}</h2>
|
|
22
|
+
{#if meta}
|
|
23
|
+
<span class="ml-0.5">{@render meta()}</span>
|
|
24
|
+
{/if}
|
|
25
|
+
</div>
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
// Re-export the pure helpers so consumers can reach them from the same entry
|
|
3
|
+
// as the component (e.g. to share the breakpoint or the shortcut predicate).
|
|
4
|
+
export { splitViewGridClass, isSearchShortcut, SPLIT_VIEW_BREAKPOINT_REM } from './split-view'
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<script lang="ts">
|
|
8
|
+
// A presentational two-pane shell — `nav | content` at `13rem | minmax(0,1fr)`,
|
|
9
|
+
// collapsing to a single column on a narrow *container* (not just viewport).
|
|
10
|
+
// It owns no routing and no domain concepts (ADR-0001): which section is
|
|
11
|
+
// active is the host's business, driven through these slots. Styling stays in
|
|
12
|
+
// Tailwind class strings and every region exposes a `*Class` override hook.
|
|
13
|
+
import type { Snippet } from 'svelte'
|
|
14
|
+
import { onMount, tick } from 'svelte'
|
|
15
|
+
import { isSearchShortcut, splitViewGridClass } from './split-view'
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** Left navigation pane content. */
|
|
19
|
+
nav?: Snippet
|
|
20
|
+
/** Right content pane. */
|
|
21
|
+
content?: Snippet
|
|
22
|
+
/** Pinned to the bottom of the nav pane. */
|
|
23
|
+
footer?: Snippet
|
|
24
|
+
/** Focusable search affordance — inline at the top of the nav by default. */
|
|
25
|
+
search?: Snippet
|
|
26
|
+
/**
|
|
27
|
+
* Force a single column (`true`) or two columns (`false`). Omit for the
|
|
28
|
+
* responsive default (collapses below the container breakpoint).
|
|
29
|
+
*/
|
|
30
|
+
collapsed?: boolean
|
|
31
|
+
/** Render the search slot as a centered overlay instead of inline. */
|
|
32
|
+
searchOverlay?: boolean
|
|
33
|
+
/** Bindable open state for the search overlay. */
|
|
34
|
+
searchOpen?: boolean
|
|
35
|
+
/**
|
|
36
|
+
* Predicate for the keyboard idiom that focuses (inline) or toggles
|
|
37
|
+
* (overlay) the search. Defaults to ⌘K / Ctrl+K. Pass `null` to disable —
|
|
38
|
+
* e.g. when the host already owns ⌘K for a global command palette.
|
|
39
|
+
*/
|
|
40
|
+
searchShortcut?: ((e: KeyboardEvent) => boolean) | null
|
|
41
|
+
/** Styling hooks. */
|
|
42
|
+
class?: string
|
|
43
|
+
navClass?: string
|
|
44
|
+
contentClass?: string
|
|
45
|
+
footerClass?: string
|
|
46
|
+
searchClass?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let {
|
|
50
|
+
nav,
|
|
51
|
+
content,
|
|
52
|
+
footer,
|
|
53
|
+
search,
|
|
54
|
+
collapsed,
|
|
55
|
+
searchOverlay = false,
|
|
56
|
+
searchOpen = $bindable(false),
|
|
57
|
+
searchShortcut = isSearchShortcut,
|
|
58
|
+
class: klass = '',
|
|
59
|
+
navClass = '',
|
|
60
|
+
contentClass = '',
|
|
61
|
+
footerClass = '',
|
|
62
|
+
searchClass = '',
|
|
63
|
+
}: Props = $props()
|
|
64
|
+
|
|
65
|
+
const gridClass = $derived(splitViewGridClass(collapsed))
|
|
66
|
+
|
|
67
|
+
let searchHost = $state<HTMLElement>()
|
|
68
|
+
let overlayHost = $state<HTMLElement>()
|
|
69
|
+
|
|
70
|
+
function focusFirst(host: HTMLElement | undefined) {
|
|
71
|
+
const el = host?.querySelector<HTMLElement>(
|
|
72
|
+
'input, textarea, select, button, [href], [tabindex]:not([tabindex="-1"])',
|
|
73
|
+
)
|
|
74
|
+
el?.focus()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function activateSearch() {
|
|
78
|
+
if (searchOverlay) {
|
|
79
|
+
searchOpen = true
|
|
80
|
+
await tick()
|
|
81
|
+
focusFirst(overlayHost)
|
|
82
|
+
} else {
|
|
83
|
+
focusFirst(searchHost)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
onMount(() => {
|
|
88
|
+
const onKey = (e: KeyboardEvent) => {
|
|
89
|
+
if (searchShortcut && searchShortcut(e)) {
|
|
90
|
+
e.preventDefault()
|
|
91
|
+
void activateSearch()
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
// Esc closes the overlay regardless of whether the shortcut is enabled.
|
|
95
|
+
if (searchOverlay && searchOpen && e.key === 'Escape') {
|
|
96
|
+
e.preventDefault()
|
|
97
|
+
searchOpen = false
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
window.addEventListener('keydown', onKey)
|
|
101
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
102
|
+
})
|
|
103
|
+
</script>
|
|
104
|
+
|
|
105
|
+
<div class="@container relative h-full min-h-0 w-full {klass}">
|
|
106
|
+
<div class="grid h-full min-h-0 {gridClass}">
|
|
107
|
+
<aside
|
|
108
|
+
class="flex min-h-0 flex-col border-line-1 bg-bg-1 border-b @min-[52rem]:border-b-0 @min-[52rem]:border-r {navClass}"
|
|
109
|
+
>
|
|
110
|
+
{#if search && !searchOverlay}
|
|
111
|
+
<div bind:this={searchHost} class="shrink-0 border-b border-line-1 p-2 {searchClass}">
|
|
112
|
+
{@render search()}
|
|
113
|
+
</div>
|
|
114
|
+
{/if}
|
|
115
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
116
|
+
{@render nav?.()}
|
|
117
|
+
</div>
|
|
118
|
+
{#if footer}
|
|
119
|
+
<div class="mt-auto shrink-0 border-t border-line-1 {footerClass}">
|
|
120
|
+
{@render footer()}
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
</aside>
|
|
124
|
+
|
|
125
|
+
<section class="min-h-0 min-w-0 overflow-auto bg-bg-0 {contentClass}">
|
|
126
|
+
{@render content?.()}
|
|
127
|
+
</section>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{#if search && searchOverlay && searchOpen}
|
|
131
|
+
<div
|
|
132
|
+
class="absolute inset-0 z-40 bg-black/50 backdrop-blur-sm"
|
|
133
|
+
role="presentation"
|
|
134
|
+
onclick={() => (searchOpen = false)}
|
|
135
|
+
></div>
|
|
136
|
+
<div
|
|
137
|
+
bind:this={overlayHost}
|
|
138
|
+
class="absolute left-1/2 top-[15%] z-50 w-[min(36rem,92%)] -translate-x-1/2 rounded-lg border border-line-2 bg-bg-1 shadow-[var(--shadow-lg)] {searchClass}"
|
|
139
|
+
role="dialog"
|
|
140
|
+
aria-modal="true"
|
|
141
|
+
>
|
|
142
|
+
{@render search()}
|
|
143
|
+
</div>
|
|
144
|
+
{/if}
|
|
145
|
+
</div>
|
package/src/lib/index.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
|
-
export { default as Button } from
|
|
2
|
-
export { default as Badge } from
|
|
3
|
-
export { default as Card } from
|
|
4
|
-
export { default as NodeBadge } from
|
|
5
|
-
export { default as Kbd } from
|
|
1
|
+
export { default as Button } from "./Button.svelte";
|
|
2
|
+
export { default as Badge } from "./Badge.svelte";
|
|
3
|
+
export { default as Card } from "./Card.svelte";
|
|
4
|
+
export { default as NodeBadge } from "./NodeBadge.svelte";
|
|
5
|
+
export { default as Kbd } from "./Kbd.svelte";
|
|
6
|
+
export { default as SplitView } from "./SplitView.svelte";
|
|
7
|
+
export { default as ListRow } from "./ListRow.svelte";
|
|
8
|
+
export { default as SectionHeading } from "./SectionHeading.svelte";
|
|
9
|
+
export { default as Pill } from "./Pill.svelte";
|
|
10
|
+
export { default as NavItem } from "./NavItem.svelte";
|
|
11
|
+
export { default as EmptyState } from "./EmptyState.svelte";
|
|
12
|
+
export { default as LoadingState } from "./LoadingState.svelte";
|
|
13
|
+
export {
|
|
14
|
+
splitViewGridClass,
|
|
15
|
+
isSearchShortcut,
|
|
16
|
+
SPLIT_VIEW_BREAKPOINT_REM,
|
|
17
|
+
} from "./split-view";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Pure, framework-free helpers for the `SplitView` shell, kept separate from
|
|
2
|
+
// the component so the grid math and the keyboard idiom can be unit-tested
|
|
3
|
+
// without mounting Svelte.
|
|
4
|
+
|
|
5
|
+
/** Container width (in `rem`) at/above which both panes show; below, one column. */
|
|
6
|
+
export const SPLIT_VIEW_BREAKPOINT_REM = 52;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Tailwind grid-template class for the two-pane shell.
|
|
10
|
+
*
|
|
11
|
+
* - `true` → always a single column
|
|
12
|
+
* - `false` → always two columns (`13rem | minmax(0,1fr)`)
|
|
13
|
+
* - omitted → responsive: one column below the container breakpoint, two at or
|
|
14
|
+
* above it (Tailwind v4 container query — the shell measures its own width,
|
|
15
|
+
* so it collapses in a narrow embed, not just a narrow viewport).
|
|
16
|
+
*
|
|
17
|
+
* Each branch returns a contiguous literal so Tailwind's source scanner emits
|
|
18
|
+
* the utilities — never assemble these class strings via interpolation.
|
|
19
|
+
*/
|
|
20
|
+
export function splitViewGridClass(collapsed?: boolean): string {
|
|
21
|
+
if (collapsed === true) return "grid-cols-1";
|
|
22
|
+
if (collapsed === false) return "grid-cols-[13rem_minmax(0,1fr)]";
|
|
23
|
+
return "grid-cols-1 @min-[52rem]:grid-cols-[13rem_minmax(0,1fr)]";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The default "Cmd+K idiom": ⌘K on macOS, Ctrl+K elsewhere, with no Shift or
|
|
28
|
+
* Alt. Returns true when a keydown should focus the inline search (or toggle
|
|
29
|
+
* the search overlay) of a `SplitView`.
|
|
30
|
+
*/
|
|
31
|
+
export function isSearchShortcut(e: KeyboardEvent): boolean {
|
|
32
|
+
return (
|
|
33
|
+
Boolean(e.metaKey || e.ctrlKey) &&
|
|
34
|
+
!e.shiftKey &&
|
|
35
|
+
!e.altKey &&
|
|
36
|
+
e.key.toLowerCase() === "k"
|
|
37
|
+
);
|
|
38
|
+
}
|