basefn 1.8.0 → 1.9.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
CHANGED
package/src/Basefn.res
CHANGED
|
@@ -62,6 +62,7 @@ type hoverCardAlign = Basefn__HoverCard.align
|
|
|
62
62
|
type alertDialogVariant = Basefn__AlertDialog.variant
|
|
63
63
|
type contextMenuItem = Basefn__ContextMenu.menuItem
|
|
64
64
|
type contextMenuContent = Basefn__ContextMenu.menuContent
|
|
65
|
+
type spotlightItem = Basefn__Spotlight.spotlightItem
|
|
65
66
|
type gridColumns = Basefn__Grid.columns
|
|
66
67
|
type gridRows = Basefn__Grid.rows
|
|
67
68
|
type gridAutoFlow = Basefn__Grid.autoFlow
|
|
@@ -227,6 +228,9 @@ module AlertDialog = {
|
|
|
227
228
|
module ContextMenu = {
|
|
228
229
|
include Basefn__ContextMenu
|
|
229
230
|
}
|
|
231
|
+
module Spotlight = {
|
|
232
|
+
include Basefn__Spotlight
|
|
233
|
+
}
|
|
230
234
|
|
|
231
235
|
// Responsive Utilities
|
|
232
236
|
module Responsive = {
|
package/src/Basefn.res.mjs
CHANGED
|
@@ -36,6 +36,7 @@ import * as Basefn__Accordion from "./components/Basefn__Accordion.res.mjs";
|
|
|
36
36
|
import * as Basefn__AppLayout from "./components/Basefn__AppLayout.res.mjs";
|
|
37
37
|
import * as Basefn__HoverCard from "./components/Basefn__HoverCard.res.mjs";
|
|
38
38
|
import * as Basefn__Separator from "./components/Basefn__Separator.res.mjs";
|
|
39
|
+
import * as Basefn__Spotlight from "./components/Basefn__Spotlight.res.mjs";
|
|
39
40
|
import * as Basefn__Breadcrumb from "./components/Basefn__Breadcrumb.res.mjs";
|
|
40
41
|
import * as Basefn__Responsive from "./Basefn__Responsive.res.mjs";
|
|
41
42
|
import * as Basefn__ScrollArea from "./components/Basefn__ScrollArea.res.mjs";
|
|
@@ -140,6 +141,8 @@ let AlertDialog = Basefn__AlertDialog;
|
|
|
140
141
|
|
|
141
142
|
let ContextMenu = Basefn__ContextMenu;
|
|
142
143
|
|
|
144
|
+
let Spotlight = Basefn__Spotlight;
|
|
145
|
+
|
|
143
146
|
let Responsive = Basefn__Responsive;
|
|
144
147
|
|
|
145
148
|
export {
|
|
@@ -188,6 +191,7 @@ export {
|
|
|
188
191
|
HoverCard,
|
|
189
192
|
AlertDialog,
|
|
190
193
|
ContextMenu,
|
|
194
|
+
Spotlight,
|
|
191
195
|
Responsive,
|
|
192
196
|
}
|
|
193
197
|
/* Not a pure module */
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
@import '../styles/variables.css';
|
|
2
|
+
|
|
3
|
+
.basefn-spotlight-backdrop {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
background-color: var(--basefn-surface-overlay);
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: flex-start;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
padding-top: 20vh;
|
|
11
|
+
z-index: 1100;
|
|
12
|
+
animation: basefn-spotlight-fade-in 0.15s ease-out;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.basefn-spotlight {
|
|
16
|
+
background: var(--basefn-bg-primary);
|
|
17
|
+
border-radius: var(--basefn-radius-xl);
|
|
18
|
+
box-shadow: var(--basefn-shadow-lg);
|
|
19
|
+
width: 560px;
|
|
20
|
+
max-width: 90vw;
|
|
21
|
+
max-height: 60vh;
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
animation: basefn-spotlight-slide-in 0.2s ease-out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Search input area */
|
|
29
|
+
.basefn-spotlight__input-wrapper {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: var(--basefn-spacing-md);
|
|
33
|
+
padding: var(--basefn-spacing-md) var(--basefn-spacing-lg);
|
|
34
|
+
border-bottom: 1px solid var(--basefn-border-primary);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.basefn-spotlight__icon {
|
|
38
|
+
flex-shrink: 0;
|
|
39
|
+
color: var(--basefn-text-tertiary);
|
|
40
|
+
width: 20px;
|
|
41
|
+
height: 20px;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.basefn-spotlight__input {
|
|
45
|
+
flex: 1;
|
|
46
|
+
border: none;
|
|
47
|
+
outline: none;
|
|
48
|
+
background: transparent;
|
|
49
|
+
font-family: var(--basefn-font-family);
|
|
50
|
+
font-size: var(--basefn-font-size-base);
|
|
51
|
+
color: var(--basefn-text-primary);
|
|
52
|
+
line-height: var(--basefn-line-height-normal);
|
|
53
|
+
padding: var(--basefn-spacing-sm) 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.basefn-spotlight__input::placeholder {
|
|
57
|
+
color: var(--basefn-text-muted);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Results list */
|
|
61
|
+
.basefn-spotlight__results {
|
|
62
|
+
overflow-y: auto;
|
|
63
|
+
padding: var(--basefn-spacing-sm) 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.basefn-spotlight__empty {
|
|
67
|
+
padding: var(--basefn-spacing-xl) var(--basefn-spacing-lg);
|
|
68
|
+
text-align: center;
|
|
69
|
+
color: var(--basefn-text-muted);
|
|
70
|
+
font-size: var(--basefn-font-size-sm);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.basefn-spotlight__group-label {
|
|
74
|
+
padding: var(--basefn-spacing-sm) var(--basefn-spacing-lg);
|
|
75
|
+
font-size: var(--basefn-font-size-xs);
|
|
76
|
+
font-weight: var(--basefn-font-weight-semibold);
|
|
77
|
+
color: var(--basefn-text-muted);
|
|
78
|
+
text-transform: uppercase;
|
|
79
|
+
letter-spacing: 0.05em;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.basefn-spotlight__item {
|
|
83
|
+
display: flex;
|
|
84
|
+
align-items: center;
|
|
85
|
+
gap: var(--basefn-spacing-md);
|
|
86
|
+
width: 100%;
|
|
87
|
+
padding: var(--basefn-spacing-sm) var(--basefn-spacing-lg);
|
|
88
|
+
margin: 0;
|
|
89
|
+
font-family: var(--basefn-font-family);
|
|
90
|
+
font-size: var(--basefn-font-size-sm);
|
|
91
|
+
color: var(--basefn-text-primary);
|
|
92
|
+
background: none;
|
|
93
|
+
border: none;
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
text-align: left;
|
|
96
|
+
transition: background-color var(--basefn-transition-fast);
|
|
97
|
+
border-radius: 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.basefn-spotlight__item:hover,
|
|
101
|
+
.basefn-spotlight__item--active {
|
|
102
|
+
background-color: var(--basefn-color-primary);
|
|
103
|
+
color: #ffffff;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.basefn-spotlight__item-icon {
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
width: 16px;
|
|
109
|
+
height: 16px;
|
|
110
|
+
color: var(--basefn-text-tertiary);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.basefn-spotlight__item:hover .basefn-spotlight__item-icon,
|
|
114
|
+
.basefn-spotlight__item--active .basefn-spotlight__item-icon {
|
|
115
|
+
color: #ffffff;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.basefn-spotlight__item-content {
|
|
119
|
+
flex: 1;
|
|
120
|
+
min-width: 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.basefn-spotlight__item-label {
|
|
124
|
+
font-weight: var(--basefn-font-weight-medium);
|
|
125
|
+
white-space: nowrap;
|
|
126
|
+
overflow: hidden;
|
|
127
|
+
text-overflow: ellipsis;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.basefn-spotlight__item-description {
|
|
131
|
+
font-size: var(--basefn-font-size-xs);
|
|
132
|
+
color: var(--basefn-text-muted);
|
|
133
|
+
white-space: nowrap;
|
|
134
|
+
overflow: hidden;
|
|
135
|
+
text-overflow: ellipsis;
|
|
136
|
+
margin-top: 1px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.basefn-spotlight__item:hover .basefn-spotlight__item-description,
|
|
140
|
+
.basefn-spotlight__item--active .basefn-spotlight__item-description {
|
|
141
|
+
color: rgba(255, 255, 255, 0.7);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Footer with keyboard hints */
|
|
145
|
+
.basefn-spotlight__footer {
|
|
146
|
+
display: flex;
|
|
147
|
+
align-items: center;
|
|
148
|
+
gap: var(--basefn-spacing-lg);
|
|
149
|
+
padding: var(--basefn-spacing-sm) var(--basefn-spacing-lg);
|
|
150
|
+
border-top: 1px solid var(--basefn-border-primary);
|
|
151
|
+
font-size: var(--basefn-font-size-xs);
|
|
152
|
+
color: var(--basefn-text-muted);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.basefn-spotlight__footer-hint {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: var(--basefn-spacing-xs);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.basefn-spotlight__footer-key {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
min-width: 1.25rem;
|
|
166
|
+
height: 1.25rem;
|
|
167
|
+
padding: 0 0.25rem;
|
|
168
|
+
border-radius: var(--basefn-radius-sm);
|
|
169
|
+
border: 1px solid var(--basefn-border-secondary);
|
|
170
|
+
background: var(--basefn-bg-secondary);
|
|
171
|
+
font-size: 0.6875rem;
|
|
172
|
+
font-family: var(--basefn-font-family);
|
|
173
|
+
line-height: 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Animations */
|
|
177
|
+
@keyframes basefn-spotlight-fade-in {
|
|
178
|
+
from { opacity: 0; }
|
|
179
|
+
to { opacity: 1; }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@keyframes basefn-spotlight-slide-in {
|
|
183
|
+
from {
|
|
184
|
+
opacity: 0;
|
|
185
|
+
transform: scale(0.98) translateY(-8px);
|
|
186
|
+
}
|
|
187
|
+
to {
|
|
188
|
+
opacity: 1;
|
|
189
|
+
transform: scale(1) translateY(0);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* Mobile */
|
|
194
|
+
@media (max-width: 640px) {
|
|
195
|
+
.basefn-spotlight-backdrop {
|
|
196
|
+
padding-top: 10vh;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.basefn-spotlight {
|
|
200
|
+
max-width: 95vw;
|
|
201
|
+
max-height: 70vh;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.basefn-spotlight__footer {
|
|
205
|
+
display: none;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
%%raw(`import './Basefn__Spotlight.css'`)
|
|
2
|
+
|
|
3
|
+
open Xote
|
|
4
|
+
|
|
5
|
+
@get external key: Dom.event => string = "key"
|
|
6
|
+
@send external focus: Dom.element => unit = "focus"
|
|
7
|
+
@send external querySelector: (Dom.element, string) => Nullable.t<Dom.element> = "querySelector"
|
|
8
|
+
|
|
9
|
+
type spotlightItem = {
|
|
10
|
+
id: string,
|
|
11
|
+
label: string,
|
|
12
|
+
description?: string,
|
|
13
|
+
group?: string,
|
|
14
|
+
onSelect: unit => unit,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@jsx.component
|
|
18
|
+
let make = (
|
|
19
|
+
~isOpen: Signal.t<bool>,
|
|
20
|
+
~onClose: unit => unit,
|
|
21
|
+
~items: array<spotlightItem>,
|
|
22
|
+
~placeholder: string="Search...",
|
|
23
|
+
~emptyMessage: string="No results found.",
|
|
24
|
+
~filterFn: option<(string, spotlightItem) => bool>=?,
|
|
25
|
+
) => {
|
|
26
|
+
let query = Signal.make("")
|
|
27
|
+
let activeIndex = Signal.make(0)
|
|
28
|
+
|
|
29
|
+
let defaultFilter = (q: string, item: spotlightItem) => {
|
|
30
|
+
let q = String.toLowerCase(q)
|
|
31
|
+
String.toLowerCase(item.label)->String.includes(q) ||
|
|
32
|
+
switch item.description {
|
|
33
|
+
| Some(desc) => String.toLowerCase(desc)->String.includes(q)
|
|
34
|
+
| None => false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let filterItem = switch filterFn {
|
|
39
|
+
| Some(fn) => fn
|
|
40
|
+
| None => defaultFilter
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let filteredItems = Computed.make(() => {
|
|
44
|
+
let q = Signal.get(query)
|
|
45
|
+
if q === "" {
|
|
46
|
+
items
|
|
47
|
+
} else {
|
|
48
|
+
items->Array.filter(item => filterItem(q, item))
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
let handleSelect = (item: spotlightItem) => {
|
|
53
|
+
item.onSelect()
|
|
54
|
+
Signal.set(query, "")
|
|
55
|
+
Signal.set(activeIndex, 0)
|
|
56
|
+
onClose()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let handleKeyDown = (evt: Dom.event) => {
|
|
60
|
+
let k = key(evt)
|
|
61
|
+
let currentItems = Signal.get(filteredItems)
|
|
62
|
+
let len = Array.length(currentItems)
|
|
63
|
+
|
|
64
|
+
switch k {
|
|
65
|
+
| "ArrowDown" => {
|
|
66
|
+
let _ = Basefn__Dom.preventDefault(evt)
|
|
67
|
+
Signal.update(activeIndex, i => mod(i + 1, max(len, 1)))
|
|
68
|
+
}
|
|
69
|
+
| "ArrowUp" => {
|
|
70
|
+
let _ = Basefn__Dom.preventDefault(evt)
|
|
71
|
+
Signal.update(activeIndex, i => mod(i - 1 + max(len, 1), max(len, 1)))
|
|
72
|
+
}
|
|
73
|
+
| "Enter" =>
|
|
74
|
+
if len > 0 {
|
|
75
|
+
let idx = Signal.get(activeIndex)
|
|
76
|
+
switch currentItems->Array.get(idx) {
|
|
77
|
+
| Some(item) => handleSelect(item)
|
|
78
|
+
| None => ()
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
| "Escape" => {
|
|
82
|
+
Signal.set(query, "")
|
|
83
|
+
Signal.set(activeIndex, 0)
|
|
84
|
+
onClose()
|
|
85
|
+
}
|
|
86
|
+
| _ => ()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let handleInput = (evt: Dom.event) => {
|
|
91
|
+
let value = Basefn__Dom.target(evt)["value"]
|
|
92
|
+
Signal.set(query, value)
|
|
93
|
+
Signal.set(activeIndex, 0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let handleBackdropClick = evt => {
|
|
97
|
+
let target = Obj.magic(evt)["target"]
|
|
98
|
+
let currentTarget = Obj.magic(evt)["currentTarget"]
|
|
99
|
+
if target === currentTarget {
|
|
100
|
+
Signal.set(query, "")
|
|
101
|
+
Signal.set(activeIndex, 0)
|
|
102
|
+
onClose()
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Auto-focus input when opened
|
|
107
|
+
let _ = Effect.run(() => {
|
|
108
|
+
if Signal.get(isOpen) {
|
|
109
|
+
let _ = setTimeout(() => {
|
|
110
|
+
let doc: Dom.element = %raw(`document.body`)
|
|
111
|
+
switch querySelector(doc, ".basefn-spotlight__input") {
|
|
112
|
+
| Value(el) => focus(el)
|
|
113
|
+
| _ => ()
|
|
114
|
+
}
|
|
115
|
+
}, 16)
|
|
116
|
+
}
|
|
117
|
+
None
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
let renderResults = () => {
|
|
121
|
+
let currentItems = Signal.get(filteredItems)
|
|
122
|
+
|
|
123
|
+
if Array.length(currentItems) === 0 {
|
|
124
|
+
<div class="basefn-spotlight__empty"> {Component.text(emptyMessage)} </div>
|
|
125
|
+
} else {
|
|
126
|
+
let lastGroup: ref<option<string>> = ref(None)
|
|
127
|
+
let elements: array<Component.node> = []
|
|
128
|
+
|
|
129
|
+
currentItems->Array.forEachWithIndex((item, index) => {
|
|
130
|
+
switch item.group {
|
|
131
|
+
| Some(group) if Some(group) !== lastGroup.contents => {
|
|
132
|
+
lastGroup := Some(group)
|
|
133
|
+
let _ = elements->Array.push(
|
|
134
|
+
<div key={"group-" ++ group} class="basefn-spotlight__group-label">
|
|
135
|
+
{Component.text(group)}
|
|
136
|
+
</div>,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
| _ => ()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let itemClass =
|
|
143
|
+
"basefn-spotlight__item" ++
|
|
144
|
+
(index === Signal.get(activeIndex) ? " basefn-spotlight__item--active" : "")
|
|
145
|
+
|
|
146
|
+
let _ = elements->Array.push(
|
|
147
|
+
<button key={item.id} class={itemClass} onClick={_ => handleSelect(item)}>
|
|
148
|
+
<div class="basefn-spotlight__item-content">
|
|
149
|
+
<div class="basefn-spotlight__item-label"> {Component.text(item.label)} </div>
|
|
150
|
+
{switch item.description {
|
|
151
|
+
| Some(desc) =>
|
|
152
|
+
<div class="basefn-spotlight__item-description"> {Component.text(desc)} </div>
|
|
153
|
+
| None => <> </>
|
|
154
|
+
}}
|
|
155
|
+
</div>
|
|
156
|
+
</button>,
|
|
157
|
+
)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
elements->Component.fragment
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let content = Computed.make(() => {
|
|
165
|
+
if Signal.get(isOpen) {
|
|
166
|
+
[
|
|
167
|
+
<div class="basefn-spotlight-backdrop" onClick={handleBackdropClick}>
|
|
168
|
+
<div class="basefn-spotlight">
|
|
169
|
+
<div class="basefn-spotlight__input-wrapper">
|
|
170
|
+
<Basefn__Icon name={Basefn__Icon.Search} size={Basefn__Icon.Sm} />
|
|
171
|
+
<input
|
|
172
|
+
class="basefn-spotlight__input"
|
|
173
|
+
type_="text"
|
|
174
|
+
placeholder
|
|
175
|
+
value={ReactiveProp.reactive(query)}
|
|
176
|
+
onInput={handleInput}
|
|
177
|
+
onKeyDown={handleKeyDown}
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
<div class="basefn-spotlight__results"> {renderResults()} </div>
|
|
181
|
+
<div class="basefn-spotlight__footer">
|
|
182
|
+
<span class="basefn-spotlight__footer-hint">
|
|
183
|
+
<span class="basefn-spotlight__footer-key"> {Component.text("\u2191")} </span>
|
|
184
|
+
<span class="basefn-spotlight__footer-key"> {Component.text("\u2193")} </span>
|
|
185
|
+
{Component.text("to navigate")}
|
|
186
|
+
</span>
|
|
187
|
+
<span class="basefn-spotlight__footer-hint">
|
|
188
|
+
<span class="basefn-spotlight__footer-key"> {Component.text("\u21b5")} </span>
|
|
189
|
+
{Component.text("to select")}
|
|
190
|
+
</span>
|
|
191
|
+
<span class="basefn-spotlight__footer-hint">
|
|
192
|
+
<span class="basefn-spotlight__footer-key"> {Component.text("esc")} </span>
|
|
193
|
+
{Component.text("to close")}
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>,
|
|
198
|
+
]
|
|
199
|
+
} else {
|
|
200
|
+
[]
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
Component.signalFragment(content)
|
|
205
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Xote from "xote/src/Xote.res.mjs";
|
|
4
|
+
import * as Xote__JSX from "xote/src/Xote__JSX.res.mjs";
|
|
5
|
+
import * as Basefn__Dom from "../Basefn__Dom.res.mjs";
|
|
6
|
+
import * as Basefn__Icon from "./Basefn__Icon.res.mjs";
|
|
7
|
+
import * as Primitive_int from "@rescript/runtime/lib/es6/Primitive_int.js";
|
|
8
|
+
|
|
9
|
+
import './Basefn__Spotlight.css'
|
|
10
|
+
;
|
|
11
|
+
|
|
12
|
+
function Basefn__Spotlight(props) {
|
|
13
|
+
let filterFn = props.filterFn;
|
|
14
|
+
let __emptyMessage = props.emptyMessage;
|
|
15
|
+
let __placeholder = props.placeholder;
|
|
16
|
+
let items = props.items;
|
|
17
|
+
let onClose = props.onClose;
|
|
18
|
+
let isOpen = props.isOpen;
|
|
19
|
+
let placeholder = __placeholder !== undefined ? __placeholder : "Search...";
|
|
20
|
+
let emptyMessage = __emptyMessage !== undefined ? __emptyMessage : "No results found.";
|
|
21
|
+
let query = Xote.Signal.make("", undefined, undefined);
|
|
22
|
+
let activeIndex = Xote.Signal.make(0, undefined, undefined);
|
|
23
|
+
let defaultFilter = (q, item) => {
|
|
24
|
+
let q$1 = q.toLowerCase();
|
|
25
|
+
if (item.label.toLowerCase().includes(q$1)) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
let desc = item.description;
|
|
29
|
+
if (desc !== undefined) {
|
|
30
|
+
return desc.toLowerCase().includes(q$1);
|
|
31
|
+
} else {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
let filterItem = filterFn !== undefined ? filterFn : defaultFilter;
|
|
36
|
+
let filteredItems = Xote.Computed.make(() => {
|
|
37
|
+
let q = Xote.Signal.get(query);
|
|
38
|
+
if (q === "") {
|
|
39
|
+
return items;
|
|
40
|
+
} else {
|
|
41
|
+
return items.filter(item => filterItem(q, item));
|
|
42
|
+
}
|
|
43
|
+
}, undefined);
|
|
44
|
+
let handleSelect = item => {
|
|
45
|
+
item.onSelect();
|
|
46
|
+
Xote.Signal.set(query, "");
|
|
47
|
+
Xote.Signal.set(activeIndex, 0);
|
|
48
|
+
onClose();
|
|
49
|
+
};
|
|
50
|
+
let handleKeyDown = evt => {
|
|
51
|
+
let k = evt.key;
|
|
52
|
+
let currentItems = Xote.Signal.get(filteredItems);
|
|
53
|
+
let len = currentItems.length;
|
|
54
|
+
switch (k) {
|
|
55
|
+
case "ArrowDown" :
|
|
56
|
+
Basefn__Dom.preventDefault(evt);
|
|
57
|
+
return Xote.Signal.update(activeIndex, i => Primitive_int.mod_(i + 1 | 0, Primitive_int.max(len, 1)));
|
|
58
|
+
case "ArrowUp" :
|
|
59
|
+
Basefn__Dom.preventDefault(evt);
|
|
60
|
+
return Xote.Signal.update(activeIndex, i => Primitive_int.mod_((i - 1 | 0) + Primitive_int.max(len, 1) | 0, Primitive_int.max(len, 1)));
|
|
61
|
+
case "Enter" :
|
|
62
|
+
if (len <= 0) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let idx = Xote.Signal.get(activeIndex);
|
|
66
|
+
let item = currentItems[idx];
|
|
67
|
+
if (item !== undefined) {
|
|
68
|
+
return handleSelect(item);
|
|
69
|
+
} else {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
case "Escape" :
|
|
73
|
+
Xote.Signal.set(query, "");
|
|
74
|
+
Xote.Signal.set(activeIndex, 0);
|
|
75
|
+
return onClose();
|
|
76
|
+
default:
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
let handleInput = evt => {
|
|
81
|
+
let value = Basefn__Dom.target(evt).value;
|
|
82
|
+
Xote.Signal.set(query, value);
|
|
83
|
+
Xote.Signal.set(activeIndex, 0);
|
|
84
|
+
};
|
|
85
|
+
let handleBackdropClick = evt => {
|
|
86
|
+
let target = evt.target;
|
|
87
|
+
let currentTarget = evt.currentTarget;
|
|
88
|
+
if (target === currentTarget) {
|
|
89
|
+
Xote.Signal.set(query, "");
|
|
90
|
+
Xote.Signal.set(activeIndex, 0);
|
|
91
|
+
return onClose();
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
Xote.Effect.run(() => {
|
|
95
|
+
if (Xote.Signal.get(isOpen)) {
|
|
96
|
+
setTimeout(() => {
|
|
97
|
+
let doc = document.body;
|
|
98
|
+
let el = doc.querySelector(".basefn-spotlight__input");
|
|
99
|
+
if (el == null) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
el.focus();
|
|
103
|
+
}, 16);
|
|
104
|
+
}
|
|
105
|
+
}, undefined);
|
|
106
|
+
let renderResults = () => {
|
|
107
|
+
let currentItems = Xote.Signal.get(filteredItems);
|
|
108
|
+
if (currentItems.length === 0) {
|
|
109
|
+
return Xote__JSX.Elements.jsx("div", {
|
|
110
|
+
class: "basefn-spotlight__empty",
|
|
111
|
+
children: Xote.Component.text(emptyMessage)
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
let lastGroup = {
|
|
115
|
+
contents: undefined
|
|
116
|
+
};
|
|
117
|
+
let elements = [];
|
|
118
|
+
currentItems.forEach((item, index) => {
|
|
119
|
+
let group = item.group;
|
|
120
|
+
if (group !== undefined && group !== lastGroup.contents) {
|
|
121
|
+
lastGroup.contents = group;
|
|
122
|
+
elements.push(Xote__JSX.Elements.jsxKeyed("div", {
|
|
123
|
+
class: "basefn-spotlight__group-label",
|
|
124
|
+
children: Xote.Component.text(group)
|
|
125
|
+
}, "group-" + group, undefined));
|
|
126
|
+
}
|
|
127
|
+
let itemClass = "basefn-spotlight__item" + (
|
|
128
|
+
index === Xote.Signal.get(activeIndex) ? " basefn-spotlight__item--active" : ""
|
|
129
|
+
);
|
|
130
|
+
let desc = item.description;
|
|
131
|
+
elements.push(Xote__JSX.Elements.jsxKeyed("button", {
|
|
132
|
+
class: itemClass,
|
|
133
|
+
onClick: param => handleSelect(item),
|
|
134
|
+
children: Xote__JSX.Elements.jsxs("div", {
|
|
135
|
+
class: "basefn-spotlight__item-content",
|
|
136
|
+
children: Xote__JSX.array([
|
|
137
|
+
Xote__JSX.Elements.jsx("div", {
|
|
138
|
+
class: "basefn-spotlight__item-label",
|
|
139
|
+
children: Xote.Component.text(item.label)
|
|
140
|
+
}),
|
|
141
|
+
desc !== undefined ? Xote__JSX.Elements.jsx("div", {
|
|
142
|
+
class: "basefn-spotlight__item-description",
|
|
143
|
+
children: Xote.Component.text(desc)
|
|
144
|
+
}) : Xote__JSX.jsx(Xote__JSX.jsxFragment, {})
|
|
145
|
+
])
|
|
146
|
+
})
|
|
147
|
+
}, item.id, undefined));
|
|
148
|
+
});
|
|
149
|
+
return Xote.Component.fragment(elements);
|
|
150
|
+
};
|
|
151
|
+
return Xote.Component.signalFragment(Xote.Computed.make(() => {
|
|
152
|
+
if (Xote.Signal.get(isOpen)) {
|
|
153
|
+
return [Xote__JSX.Elements.jsx("div", {
|
|
154
|
+
class: "basefn-spotlight-backdrop",
|
|
155
|
+
onClick: handleBackdropClick,
|
|
156
|
+
children: Xote__JSX.Elements.jsxs("div", {
|
|
157
|
+
class: "basefn-spotlight",
|
|
158
|
+
children: Xote__JSX.array([
|
|
159
|
+
Xote__JSX.Elements.jsxs("div", {
|
|
160
|
+
class: "basefn-spotlight__input-wrapper",
|
|
161
|
+
children: Xote__JSX.array([
|
|
162
|
+
Xote__JSX.jsx(Basefn__Icon.make, {
|
|
163
|
+
name: "Search",
|
|
164
|
+
size: "Sm"
|
|
165
|
+
}),
|
|
166
|
+
Xote__JSX.Elements.jsx("input", {
|
|
167
|
+
class: "basefn-spotlight__input",
|
|
168
|
+
type: "text",
|
|
169
|
+
value: Xote.ReactiveProp.reactive(query),
|
|
170
|
+
placeholder: placeholder,
|
|
171
|
+
onInput: handleInput,
|
|
172
|
+
onKeyDown: handleKeyDown
|
|
173
|
+
})
|
|
174
|
+
])
|
|
175
|
+
}),
|
|
176
|
+
Xote__JSX.Elements.jsx("div", {
|
|
177
|
+
class: "basefn-spotlight__results",
|
|
178
|
+
children: renderResults()
|
|
179
|
+
}),
|
|
180
|
+
Xote__JSX.Elements.jsxs("div", {
|
|
181
|
+
class: "basefn-spotlight__footer",
|
|
182
|
+
children: Xote__JSX.array([
|
|
183
|
+
Xote__JSX.Elements.jsxs("span", {
|
|
184
|
+
class: "basefn-spotlight__footer-hint",
|
|
185
|
+
children: Xote__JSX.array([
|
|
186
|
+
Xote__JSX.Elements.jsx("span", {
|
|
187
|
+
class: "basefn-spotlight__footer-key",
|
|
188
|
+
children: Xote.Component.text("\u2191")
|
|
189
|
+
}),
|
|
190
|
+
Xote__JSX.Elements.jsx("span", {
|
|
191
|
+
class: "basefn-spotlight__footer-key",
|
|
192
|
+
children: Xote.Component.text("\u2193")
|
|
193
|
+
}),
|
|
194
|
+
Xote.Component.text("to navigate")
|
|
195
|
+
])
|
|
196
|
+
}),
|
|
197
|
+
Xote__JSX.Elements.jsxs("span", {
|
|
198
|
+
class: "basefn-spotlight__footer-hint",
|
|
199
|
+
children: Xote__JSX.array([
|
|
200
|
+
Xote__JSX.Elements.jsx("span", {
|
|
201
|
+
class: "basefn-spotlight__footer-key",
|
|
202
|
+
children: Xote.Component.text("\u21b5")
|
|
203
|
+
}),
|
|
204
|
+
Xote.Component.text("to select")
|
|
205
|
+
])
|
|
206
|
+
}),
|
|
207
|
+
Xote__JSX.Elements.jsxs("span", {
|
|
208
|
+
class: "basefn-spotlight__footer-hint",
|
|
209
|
+
children: Xote__JSX.array([
|
|
210
|
+
Xote__JSX.Elements.jsx("span", {
|
|
211
|
+
class: "basefn-spotlight__footer-key",
|
|
212
|
+
children: Xote.Component.text("esc")
|
|
213
|
+
}),
|
|
214
|
+
Xote.Component.text("to close")
|
|
215
|
+
])
|
|
216
|
+
})
|
|
217
|
+
])
|
|
218
|
+
})
|
|
219
|
+
])
|
|
220
|
+
})
|
|
221
|
+
})];
|
|
222
|
+
} else {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}, undefined));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let make = Basefn__Spotlight;
|
|
229
|
+
|
|
230
|
+
export {
|
|
231
|
+
make,
|
|
232
|
+
}
|
|
233
|
+
/* Not a pure module */
|