Skip to content

Commit f820b29

Browse files
committed
command_palette: Add base markup
1 parent 44f8907 commit f820b29

File tree

7 files changed

+319
-127
lines changed

7 files changed

+319
-127
lines changed

lib/core/src/types.ts

+23-15
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ declare global {
219219
*/
220220
interface cmd {
221221
command_palette: {
222-
add: { args: Ordo.CommandPalette.Item }
222+
add: { args: Ordo.CommandPalette.Item<() => void> }
223223
remove: { args: string | number }
224224
toggle: { args: void }
225225
show: { args: Ordo.CommandPalette.Instance | undefined }
@@ -1152,36 +1152,44 @@ declare global {
11521152
}
11531153

11541154
namespace CommandPalette {
1155-
type Instance = {
1156-
items: Ordo.CommandPalette.Item[]
1157-
on_new_item?: (input: string) => Ordo.CommandPalette.Item
1155+
type Instance<$Value = any> = {
1156+
items: Ordo.CommandPalette.Item<$Value>[]
1157+
on_new_item?: (input: string) => Ordo.CommandPalette.Item<$Value>
11581158
is_multiple?: boolean
1159-
on_select: (item: Ordo.CommandPalette.Item) => void
1160-
on_deselect?: (item: Ordo.CommandPalette.Item) => void
1161-
pinned_items?: Ordo.CommandPalette.Item[]
1159+
on_select: (item: Ordo.CommandPalette.Item<$Value>) => void
1160+
on_deselect?: (item: Ordo.CommandPalette.Item<$Value>) => void
1161+
pinned_items?: Ordo.CommandPalette.Item<$Value>[]
11621162
max_items?: number
11631163
}
11641164

1165+
type Id = string | number
1166+
1167+
type RenderIcon = (span: HTMLSpanElement) => void | Promise<void>
1168+
1169+
type RenderCustomItemFooter = (div: HTMLDivElement) => void | Promise<void>
1170+
1171+
type RenderCustomItemInfo = (div: HTMLDivElement) => void | Promise<void>
1172+
11651173
/**
11661174
* Command palette item.
11671175
*/
1168-
type Item<T = any> = {
1169-
id: string | number
1176+
type Item<$Value = any> = {
1177+
id: Ordo.CommandPalette.Id
11701178
/**
11711179
* Readable name of the command palette item. Put a translation key here, if you use i18n.
11721180
*/
1173-
readable_name: Ordo.I18N.TranslationKey
1181+
readable_name: string
11741182

1175-
value: T
1183+
value: $Value
11761184

11771185
/**
11781186
* Icon to be displayed for the context menu item.
11791187
*
11801188
* @optional
11811189
*/
1182-
render_icon?: (span: HTMLSpanElement) => void | Promise<void>
1183-
render_custom_footer?: (div: HTMLDivElement) => void | Promise<void>
1184-
render_custom_info?: (div: HTMLDivElement) => void | Promise<void>
1190+
render_icon?: Ordo.CommandPalette.RenderIcon
1191+
render_custom_footer?: Ordo.CommandPalette.RenderCustomItemFooter
1192+
render_custom_info?: Ordo.CommandPalette.RenderCustomItemFooter
11851193

11861194
/**
11871195
* Keyboard hotkey for the context menu item. It only works while the context menu is
@@ -1191,7 +1199,7 @@ declare global {
11911199
*/
11921200
hotkey?: string
11931201

1194-
description?: Ordo.I18N.TranslationKey
1202+
description?: string
11951203

11961204
type?: C.CommandPaletteItemType
11971205
}
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,17 @@
11
import { Maoka, maoka } from "@ordo-pink/maoka"
2+
import { create_hotkey_from_event } from "@ordo-pink/hotkey-from-event"
23
import { maoka_jabs } from "@ordo-pink/maoka-jabs"
4+
import { sweech } from "@ordo-pink/sweech"
5+
import { title_case } from "@ordo-pink/tau"
36

4-
import { app_context } from "@ordo-pink/frontend-app/app-context"
7+
import { app_context } from "../../app-context"
58
import { command_palette$ } from "./command-palette.state"
69

7-
export const command_palette = () =>
8-
internal.command_palette_wrapper(() =>
9-
internal.command_palette_window(() => [
10-
internal.command_palette_search(),
11-
internal.command_palette_items_div(() => () => "Items"),
12-
internal.command_palette_footer_div(() => () => "Footer"),
13-
]),
14-
)
10+
export const command_palette = () => internal.overlay(internal.modal)
1511

1612
namespace internal {
17-
export const command_palette_wrapper: Maoka.Teacher = kindergarten =>
18-
styled.command_palette_wrapper(use => {
13+
export const overlay: Maoka.Teacher = kindergarten =>
14+
styled.overlay(use => {
1915
const { hunter } = use(app_context.consume)
2016

2117
const handle_show = () => use(maoka_jabs.add_class("active"))
@@ -29,21 +25,59 @@ namespace internal {
2925
return kindergarten
3026
})
3127

32-
export const command_palette_window: Maoka.Teacher = kindergarten =>
33-
styled.command_palette_window(use => {
34-
const handle_click = (event: MouseEvent) => event.stopPropagation()
35-
28+
export const modal = () =>
29+
styled.modal(use => {
3630
const get_current = use(maoka_jabs.cheat$(command_palette$, "current" as const))
3731

32+
const handle_click = (event: MouseEvent) => event.stopPropagation()
33+
3834
use(maoka_jabs.set_id("cp"))
3935
use(maoka_jabs.listen("onclick", handle_click))
4036

41-
return () => get_current() && kindergarten()
37+
return () => {
38+
const current = get_current()
39+
40+
if (!current) return null
41+
42+
return [
43+
search(),
44+
// TODO open via route fragment and query
45+
// TODO create subitems if item is found with fuzzy search but the match is not exact
46+
current.is_multiple
47+
? items_wrapper(() => [items(() => current.items.map(item)), items(() => current.pinned_items?.map(item))])
48+
: items(() => current.items.map(item)),
49+
styled.footer(() => () => "Footer"),
50+
]
51+
}
52+
})
53+
54+
const item = (item: Ordo.CommandPalette.Item) =>
55+
styled.item(use => {
56+
const handle_click = () => item.value()
57+
58+
use(maoka_jabs.set_id(String(item.id)))
59+
use(maoka_jabs.set_attribute("title", item.description))
60+
use(maoka_jabs.listen("onclick", handle_click))
61+
62+
return () => [
63+
item_main(() => [
64+
styled.item_title_wrapper(() => () => [item.render_icon && item_icon_span(item.render_icon), item.readable_name]),
65+
item.hotkey && styled.item_info(() => () => hotkey(item.hotkey!, { decoration_only: true })),
66+
]),
67+
item_footer(() => item.description),
68+
]
4269
})
4370

44-
export const command_palette_search = () =>
45-
command_palette_form(() =>
46-
styled.command_palette_input(use => {
71+
const item_main: Maoka.Teacher = kindergarten => styled.item_main(() => kindergarten)
72+
73+
const item_footer: Maoka.Teacher = kindergarten => styled.item_footer(() => kindergarten)
74+
75+
const item_icon_span = (render_icon: Ordo.CommandPalette.RenderIcon) =>
76+
maoka.create("span", use => use(maoka.jabs.if_dom(n => void render_icon(n.value))))
77+
78+
const search = () =>
79+
form(() =>
80+
styled.item_input(use => {
4781
const t_search = "Search..." // TODO i18n
4882

4983
const handle_mount = () => use(maoka.jabs.if_dom(n => n.value.focus()))
@@ -55,8 +89,8 @@ namespace internal {
5589
}),
5690
)
5791

58-
const command_palette_form: Maoka.Teacher = kindergarten =>
59-
styled.command_palette_form(use => {
92+
const form: Maoka.Teacher = kindergarten =>
93+
styled.item_form(use => {
6094
const handle_submit = (event: Event) => event.preventDefault()
6195

6296
use(maoka_jabs.set_id("cp-form"))
@@ -65,13 +99,108 @@ namespace internal {
6599
return kindergarten
66100
})
67101

68-
export const command_palette_items_div = maoka.styled.div("command-palette_items")
69-
export const command_palette_footer_div = maoka.styled.div("command-palette_footer")
102+
const items_wrapper: Maoka.Teacher = kindergarten => styled.items_wrapper(() => kindergarten)
103+
104+
const items: Maoka.Teacher = kindergarten => styled.items(() => kindergarten)
70105

71106
namespace styled {
72-
export const command_palette_form = maoka.styled.form("command-palette_form")
73-
export const command_palette_input = maoka.styled.input("command-palette_search")
74-
export const command_palette_window = maoka.styled.div("command-palette")
75-
export const command_palette_wrapper = maoka.styled.div("command-palette_wrapper")
107+
export const items_wrapper = maoka.styled.div("command-palette_items_multiple-wrapper")
108+
export const items = maoka.styled.div("command-palette_items")
109+
export const item = maoka.styled.div("command-palette_item")
110+
export const item_info = maoka.styled.div("command-palette_item_info")
111+
export const item_title_wrapper = maoka.styled.div("command-palette_item_title-wrapper")
112+
export const item_main = maoka.styled.div("command-palette_item_main")
113+
export const item_footer = maoka.styled.div("command-palette_item_footer")
114+
export const item_form = maoka.styled.form("command-palette_form")
115+
export const item_input = maoka.styled.input("command-palette_search")
116+
export const modal = maoka.styled.div("command-palette")
117+
export const overlay = maoka.styled.div("command-palette_wrapper")
118+
export const footer = maoka.styled.div("command-palette_footer")
76119
}
77120
}
121+
122+
// TODO Move to core
123+
124+
export type HotkeyOptions = {
125+
prevent_in_inputs?: boolean
126+
prevent_in_contenteditable?: boolean
127+
decoration_only?: boolean
128+
show_in_mobile?: boolean
129+
}
130+
131+
const hotkey = (hotkey: string, options?: HotkeyOptions) =>
132+
hotkey_div((use, node) => {
133+
const split = hotkey.split("+")
134+
135+
const is_darwin = use(maoka_jabs.is_darwin)
136+
137+
const meta = is_darwin ? Key("⌥") : Key("Alt")
138+
const mod = is_darwin ? Key("⌘") : Key("Ctrl")
139+
const ctrl = Key("Ctrl")
140+
const option = Key("⌥")
141+
const shift = Key("⇧")
142+
143+
const symbol = split[split.length - 1].toLowerCase()
144+
145+
const handle_mount = () => {
146+
const handle_keydown = (event: KeyboardEvent) => {
147+
if (IGNORED_KEYS.includes(event.key) || options?.decoration_only) return
148+
149+
if (options?.prevent_in_inputs) {
150+
const target = event.target as HTMLElement
151+
152+
// TODO Add textarea and div contenteditable
153+
if (target.tagName === "INPUT") return
154+
}
155+
156+
const parsed_hotkey = create_hotkey_from_event(event, is_darwin)
157+
158+
if (parsed_hotkey === hotkey) {
159+
event.preventDefault()
160+
161+
if (node.value instanceof globalThis.HTMLElement) node.value.click()
162+
}
163+
}
164+
165+
document.addEventListener("keydown", handle_keydown)
166+
167+
return () => {
168+
document.removeEventListener("keydown", handle_keydown)
169+
}
170+
}
171+
172+
if (options?.show_in_mobile) use(maoka_jabs.add_class("mobile"))
173+
if (!options?.decoration_only) use(maoka.jabs.onmount(handle_mount))
174+
175+
return () => [
176+
split.includes("ctrl") ? ctrl : void 0,
177+
split.includes("meta") ? meta : void 0,
178+
split.includes("option") ? option : void 0,
179+
split.includes("mod") ? mod : void 0,
180+
split.includes("shift") ? shift : void 0,
181+
182+
Key(symbol),
183+
]
184+
})
185+
186+
const hotkey_div = maoka.styled.div("hotkey")
187+
188+
const IGNORED_KEYS = ["Control", "Shift", "Alt", "Meta"]
189+
190+
const KeyContainer = maoka.styled.span("key-container")
191+
192+
const Key = (key: string) =>
193+
KeyContainer(
194+
() => () =>
195+
sweech
196+
.match(key)
197+
.case("backspace", () => "⌫")
198+
.case("enter", () => "⏎")
199+
.case("escape", () => "Esc")
200+
.case("tab", () => "⇥")
201+
.case("arrowleft", () => "←")
202+
.case("arrowright", () => "→")
203+
.case("arrowup", () => "↑")
204+
.case("arrowdown", () => "↓")
205+
.default(() => title_case(key)),
206+
)

lib/frontend-app/src/command-palette/command-palette.jab.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { Maoka, maoka } from "@ordo-pink/maoka"
2+
import { CommandPaletteItemType } from "@ordo-pink/core"
23

34
import { app_context } from "../../app-context"
45
import { command_palette } from "./command-palette.component"
56
import { command_palette$ } from "./command-palette.state"
67

78
import "./command-palette.style.css"
9+
import { BsTerminal } from "@ordo-pink/frontend-icons"
810

911
export const create_command_palette: Maoka.Jab = use => {
1012
use(internal.track_prey)
@@ -23,7 +25,19 @@ namespace internal {
2325
const release_show = hunter.track("command_palette.show", guns.command_palette_show)
2426
const release_toggle = hunter.track("command_palette.toggle", guns.command_palette_toggle)
2527

28+
hunter.shoot("command_palette.add", {
29+
id: COMMAND_PALETTE_TOGGLE_ID,
30+
readable_name: "Toggle command palette",
31+
value: () => hunter.shoot("command_palette.toggle"),
32+
hotkey: "mod+shift+p",
33+
description: "Show or hide command palette. Hides command palette if you can see this message.",
34+
type: CommandPaletteItemType.MODAL_OPENER,
35+
render_icon: span => maoka.dom.render(span, BsTerminal(), () => crypto.randomUUID()),
36+
})
37+
2638
return () => {
39+
hunter.shoot("command_palette.remove", COMMAND_PALETTE_TOGGLE_ID)
40+
2741
release_add()
2842
release_hide()
2943
release_remove()
@@ -35,8 +49,10 @@ namespace internal {
3549
use(maoka.jabs.onmount(handle_onmount))
3650
}
3751

52+
const COMMAND_PALETTE_TOGGLE_ID = "command_palette.toggle"
53+
3854
// TODO Check how to select global items
39-
const global_palette = (): Ordo.CommandPalette.Instance => ({
55+
const global_palette = (): Ordo.CommandPalette.Instance<() => void> => ({
4056
items: command_palette$.select("items"),
4157
on_select: item => item.value(),
4258
})

0 commit comments

Comments
 (0)