Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions Library/Std/Widgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,92 @@ event.listen {
}
```

# Page Hierarchy
```space-lua
-- priority: 10
widgets = widgets or {}

function widgets.pageHierarchy(pageName)
pageName = pageName or editor.getCurrentPage()

if not string.find(pageName, "/") then
return nil
end

local hierarchyItems = {}

-- Extract parent path (everything before the last "/")
local parentPath = string.match(pageName, "^(.+)/[^/]+$")
if parentPath then
-- Always add the immediate parent as the first item
local parentDisplayName = parentPath:gsub("/", " / ")
table.insert(hierarchyItems, "[[" .. parentPath .. "|" .. parentDisplayName .. "]]")
end

-- Query for all descendant pages that start with current page name + "/"
local descendantPages = query[[
from index.tag "page"
where string.startsWith(_.name, pageName .. "/")
and not string.startsWith(_.name, "_")
order by name
]]

-- Add descendant pages
for _, page in ipairs(descendantPages) do
local displayName = page.name:gsub("/", " / ")
table.insert(hierarchyItems, "[[" .. page.name .. "|" .. displayName .. "]]")
end

if #hierarchyItems > 0 then
-- HTML instead of Markdown to make it collapsible
local html = "<div class=\"collapsible-hierarchy collapsed\">" ..
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't the multiline string syntax be better suited here? [[ ... ]]

"<h1 onclick=\"sbWidgets.toggleHierarchy(this)\" role=\"button\" aria-expanded=\"false\" tabindex=\"0\" onkeydown=\"if(event.key==='Enter'||event.key===' ') sbWidgets.toggleHierarchy(this)\"><span class=\"chevron-icon\"></span> Hierarchy</h1>" ..
"<div class=\"hierarchy-content\" role=\"region\" aria-label=\"Page hierarchy list\"><ul>"

for _, item in ipairs(hierarchyItems) do
-- Markdown link to HTML link
local pageName, displayName = item:match("%[%[([^|]+)|([^%]]+)%]%]")
if pageName and displayName then
-- Split path into clickable segments
local pathSegments = {}
for segment in string.gmatch(pageName, "([^/]+)") do
table.insert(pathSegments, segment)
end

local linkParts = {}
local currentPath = ""

for i, segment in ipairs(pathSegments) do
if i > 1 then
currentPath = currentPath .. "/"
end
currentPath = currentPath .. segment

table.insert(linkParts, "<a href=\"" .. currentPath .. "\" data-ref=\"" .. currentPath .. "\">" .. segment .. "</a>")
end

html = html .. "<li>" .. table.concat(linkParts, " / ") .. "</li>"
end
end

html = html .. "</ul></div></div>"

return widget.new {
html = html
}
end

return nil
end

event.listen {
name = "hooks:renderBottomWidgets",
run = function(e)
return widgets.pageHierarchy()
end
}
```

# Linked mentions
```space-lua
-- priority: 10
Expand Down
25 changes: 25 additions & 0 deletions web/boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,31 @@ safeRun(async () => {
);
// @ts-ignore: on purpose
globalThis.client = client;

// Widget utilities for collapsible sections
// @ts-ignore: on purpose
globalThis.sbWidgets = globalThis.sbWidgets || {};

// Toggle function for collapsible hierarchy
// @ts-ignore: on purpose
globalThis.sbWidgets.toggleHierarchy = function(header: HTMLElement) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a little misplaced imo. This could just be in the widget code as a script tag, couldn't it?

try {
const widget = header.closest('.collapsible-hierarchy') as HTMLElement;
if (!widget) return;

const isCollapsed = widget.classList.contains('collapsed');
if (isCollapsed) {
widget.classList.remove('collapsed');
header.setAttribute('aria-expanded', 'true');
} else {
widget.classList.add('collapsed');
header.setAttribute('aria-expanded', 'false');
}
} catch (error) {
console.error('Error toggling hierarchy:', error);
}
};

await client.init();
});

Expand Down
20 changes: 17 additions & 3 deletions web/cm_plugins/lua_widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ export class LuaWidget extends WidgetType {
if (widgetContent.cssClasses) {
div.className = widgetContent.cssClasses.join(" ");
}
let htmlFromHtml: HTMLElement | undefined;
let htmlFromMarkdown: HTMLElement | undefined;

if (widgetContent.html) {
html = typeof widgetContent.html === "string"
htmlFromHtml = typeof widgetContent.html === "string"
? parseHtmlString(widgetContent.html)
: widgetContent.html;

Expand Down Expand Up @@ -167,7 +170,7 @@ export class LuaWidget extends WidgetType {
trimmedMarkdown,
);

html = parseHtmlString(renderMarkdownToHtml(mdTree, {
htmlFromMarkdown = parseHtmlString(renderMarkdownToHtml(mdTree, {
translateUrls: (url) => {
if (isLocalPath(url)) {
url = resolvePath(
Expand All @@ -181,7 +184,18 @@ export class LuaWidget extends WidgetType {
preserveAttributes: true,
}, this.client.ui.viewState.allPages));
}
if (html) {

// Combine HTML from both sources: Markdown and HTML
if (htmlFromHtml || htmlFromMarkdown) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like weird behaviour to me, it's not intuitive.
Imo, if the user decides he wants to export raw html, but still include markdown they should use the markdownToHtml syscall. 90% of the time just concatenating it in a div is not going to be what you want.

if (htmlFromHtml && htmlFromMarkdown) {
const combinedWrapper = document.createElement("div");
combinedWrapper.appendChild(htmlFromHtml);
combinedWrapper.appendChild(htmlFromMarkdown);
html = combinedWrapper;
} else {
// Only one type of content
html = htmlFromHtml || htmlFromMarkdown!;
}
div.replaceChildren(this.wrapHtml(block, html));
attachWidgetEventHandlers(
div,
Expand Down
60 changes: 60 additions & 0 deletions web/styles/main.scss
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,63 @@ body {
.sb-markdown-toolbar:hover {
opacity: 1;
}

/* Collapsible hierarchy widget styles */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be put into a style tag in the lua. Imo, keeping as much out of SB itself is the main objective

.collapsible-hierarchy {
margin: 1rem 0 2rem;

h1 {
margin: 0;
padding: 0.5rem 0;
font-size: 1.2rem;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 0.5rem;

&:hover {
background-color: var(--ui-background-color);
}

&:focus {
outline: 2px solid var(--accent-color);
outline-offset: 2px;
}
}

.chevron-icon {
display: inline-block;
width: 32px;
height: 32px;
margin-right: 8px;
vertical-align: text-bottom;
background-color: currentColor;
-webkit-mask: var(--chevron-right-icon) no-repeat center;
mask: var(--chevron-right-icon) no-repeat center;
-webkit-mask-size: 70%;
mask-size: 70%;
}

.hierarchy-content {
overflow: hidden;
transition: max-height 0.3s ease-in-out;
max-height: 1000px;
}

&:not(.collapsed) {
.chevron-icon {
-webkit-mask: var(--chevron-down-icon) no-repeat center;
mask: var(--chevron-down-icon) no-repeat center;
-webkit-mask-size: 70%;
mask-size: 70%;
}
}

&.collapsed {

.hierarchy-content {
max-height: 0;
}
}
}
6 changes: 6 additions & 0 deletions web/styles/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,9 @@ html[data-theme="dark"] {
--admonition-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>');
--admonition-color: #ff9100;
}

/* Chevron icons for collapsible sections */
html {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the other comments

--chevron-right-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9,18 15,12 9,6"></polyline></svg>');
--chevron-down-icon: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6,9 12,15 18,9"></polyline></svg>');
}