From 8849f0736f8b431b618aa1ec0ba81de3d3bd08b4 Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Wed, 18 Jun 2025 16:58:45 +0200 Subject: [PATCH] New actionButton.define command --- Library/Std/Action Button.md | 39 +++++++++++++++++++++++++++++++++++ Library/Std/Config.md | 7 +++++++ lib/space_lua/stdlib/table.ts | 9 +++++--- web/client.ts | 1 - web/editor_ui.tsx | 25 +++++++++++++++------- 5 files changed, 69 insertions(+), 12 deletions(-) create mode 100644 Library/Std/Action Button.md diff --git a/Library/Std/Action Button.md b/Library/Std/Action Button.md new file mode 100644 index 00000000..ca26e8b9 --- /dev/null +++ b/Library/Std/Action Button.md @@ -0,0 +1,39 @@ +#meta + +APIs to more conveniently define action buttons (alternative to setting them via regular [[^Library/Std/Config]]). + +# API +## actionButton.define(spec) +Keys to define: + +* `icon`: [feather icon](https://feathericons.com) to use for your button +* `run`: function to invoke once the button is pushed +* `description` (optional): description of button (appears on hover) +* `priority` (optional): determines priority of button (the higher, the earlier in the list) +* `mobile`: when set to `true` this button will only appear on mobile devices + +# Example +```lua +-- Defines a new button that forces your UI into read-only mode +actionButton.define { + icon = "eye", + -- Uncomment the following line to only make this button appear + -- ONLY on mobile devices: + -- mobile = true, + run = function() + editor.setUiOption("forcedROMode", true) + editor.rebuildEditorState() + end +} +``` +# Implementation +```space-lua +-- priority: 100 + +actionButton = actionButton or {} + +function actionButton.define(spec) + local actionButtonConfig = config.get("actionButtons", {}) + table.insert(actionButtonConfig, spec) +end +``` \ No newline at end of file diff --git a/Library/Std/Config.md b/Library/Std/Config.md index 1f60eb44..1875101a 100644 --- a/Library/Std/Config.md +++ b/Library/Std/Config.md @@ -144,6 +144,10 @@ config.define("actionButtons", { type = "string", description = "Optional description of the action button" }, + priority = { + type = "number", + description = "Optional priority: the higher the earlier the button will appear in the list" + }, mobile = { type = "boolean", description = "Optional boolean indicating if the action button is applicable for mobile" @@ -262,6 +266,7 @@ config.set { { icon = "home", description = "Go to the index page", + priority = 3, run = function() editor.invokeCommand("Navigate: Home") end @@ -269,6 +274,7 @@ config.set { { icon = "book", description = "Open page", + priority = 2, run = function() editor.invokeCommand("Navigate: Page Picker") end @@ -276,6 +282,7 @@ config.set { { icon = "terminal", description = "Run command", + priority = 1, run = function() editor.invokeCommand "Open Command Palette" end, diff --git a/lib/space_lua/stdlib/table.ts b/lib/space_lua/stdlib/table.ts index df5930ae..107496cc 100644 --- a/lib/space_lua/stdlib/table.ts +++ b/lib/space_lua/stdlib/table.ts @@ -8,6 +8,7 @@ import { LuaRuntimeError, LuaTable, type LuaValue, + luaValueToJS, } from "../runtime.ts"; import { asyncQuickSort } from "../util.ts"; @@ -42,12 +43,14 @@ export const tableApi = new LuaTable({ * @param value - The value to insert. */ insert: new LuaBuiltinFunction( - (_sf, tbl: LuaTable | any[], posOrValue: number | any, value?: any) => { + (sf, tbl: LuaTable | any[], posOrValue: number | any, value?: any) => { if (Array.isArray(tbl)) { + // Since we're inserting/appending to a native JS array, we'll also convert the value to a JS value on the fly + // this seems like a reasonable heuristic if (value === undefined) { - tbl.push(posOrValue); + tbl.push(luaValueToJS(posOrValue, sf)); } else { - tbl.splice(posOrValue - 1, 0, value); + tbl.splice(posOrValue - 1, 0, luaValueToJS(value, sf)); } } else if (tbl instanceof LuaTable) { if (value === undefined) { diff --git a/web/client.ts b/web/client.ts index 412083ff..dfccd605 100644 --- a/web/client.ts +++ b/web/client.ts @@ -445,7 +445,6 @@ export class Client { if ( !this.ui.viewState.unsavedChanges || - this.ui.viewState.uiOptions.forcedROMode || this.clientConfig.readOnly ) { // No unsaved changes, or read-only mode, not gonna save diff --git a/web/editor_ui.tsx b/web/editor_ui.tsx index 2d33d67e..2e7dfa11 100644 --- a/web/editor_ui.tsx +++ b/web/editor_ui.tsx @@ -130,7 +130,10 @@ export class MainUI { // Need to dispatch a resize event so that the top_bar can pick it up globalThis.dispatchEvent(new Event("resize")); }, [viewState.panels]); - + const actionButtons = client.config.get( + "actionButtons", + [], + ); return ( <> {viewState.showPageNavigator && ( @@ -335,15 +338,20 @@ export class MainUI { }] : [], // Custom action buttons - ...client.config.get( - "actionButtons", - [], - ).filter(( + ...actionButtons.filter(( // Filter out buttons without icons (invalid) and mobile buttons when not in mobile mode button, ) => - (typeof button.mobile === "undefined") || - (button.mobile === viewState.isMobile) + button.icon && ( + (typeof button.mobile === "undefined") || + (button.mobile === viewState.isMobile) + ) ) + // Then ensure all buttons have a priority set (by default based on array index) + .map((button, index) => ({ + ...button, + priority: button.priority ?? actionButtons.length - index, + })) + .sort((a, b) => b.priority - a.priority) .map((button) => { const mdiIcon = (mdi as any)[kebabToCamel(button.icon)]; let featherIcon = @@ -356,7 +364,7 @@ export class MainUI { description: button.description || "", callback: button.run || (() => { client.flashNotification( - "Legacy actionButton definition detected, please define a run() callback", + "actionButton did not specify a run() callback", "error", ); }), @@ -427,6 +435,7 @@ type ActionButton = { icon: string; description?: string; mobile?: boolean; + priority?: number; run: () => void; };