From 70cb0fc22652e4fca9ae8f21bf4c94c9712bb021 Mon Sep 17 00:00:00 2001 From: onespaceman Date: Wed, 31 Jul 2024 05:28:31 -0400 Subject: [PATCH] Per-tag page styling (#945) PageDecorator add cssClasses --------- Co-authored-by: Zef Hemel --- lib/web.ts | 1 + plug-api/types.ts | 3 ++- plugs/editor/complete.ts | 13 +++++++---- plugs/markdown/markdown_render.ts | 7 ++++++ web/client.ts | 20 ++++++++++++++-- web/cm_plugins/wiki_link.ts | 16 ++++++++++--- web/components/filter.tsx | 7 +++--- web/components/page_navigator.tsx | 10 +++++++- web/components/top_bar.tsx | 9 ++++++-- web/editor_state.ts | 8 +++++++ web/editor_ui.tsx | 4 ++++ website/Object Decorators.md | 5 ++-- website/Page Decorations.md | 38 +++++++++++++++++++++++++++---- website/SETTINGS.md | 4 ++-- 14 files changed, 120 insertions(+), 25 deletions(-) diff --git a/lib/web.ts b/lib/web.ts index 0d0ba40d..088930ee 100644 --- a/lib/web.ts +++ b/lib/web.ts @@ -4,6 +4,7 @@ export type FilterOption = { description?: string; orderId?: number; hint?: string; + classes?: string; } & Record; export type Notification = { diff --git a/plug-api/types.ts b/plug-api/types.ts index 02c474f6..5d78eaed 100644 --- a/plug-api/types.ts +++ b/plug-api/types.ts @@ -26,7 +26,8 @@ export type PageMeta = ObjectValue< * Decorates a page when it matches certain criteria */ export type PageDecoration = { - prefix: string; + prefix?: string; + cssClasses?: string[]; hide?: boolean; }; diff --git a/plugs/editor/complete.ts b/plugs/editor/complete.ts index eb0e4d33..aef258be 100644 --- a/plugs/editor/complete.ts +++ b/plugs/editor/complete.ts @@ -124,10 +124,10 @@ export async function pageComplete(completeEvent: CompleteEvent) { from: completeEvent.pos - prefix.length, options: allPages.map((pageMeta) => { const completions: any[] = []; - let namePrefix = ""; - if ((pageMeta as PageMeta).pageDecoration?.prefix) { - namePrefix = pageMeta.pageDecoration?.prefix; - } + const namePrefix = (pageMeta as PageMeta).pageDecoration?.prefix || ""; + const cssClass = ((pageMeta as PageMeta).pageDecoration?.cssClasses || []) + .join(" ").replaceAll(/[^a-zA-Z0-9-_ ]/g, ""); + if (isWikilink) { // A [[wikilink]] if (pageMeta.displayName) { @@ -141,6 +141,7 @@ export async function pageComplete(completeEvent: CompleteEvent) { : `${pageMeta.name}|${pageMeta.displayName}`, detail: `displayName for: ${pageMeta.name}`, type: "page", + cssClass, }); } if (Array.isArray(pageMeta.aliases)) { @@ -155,6 +156,7 @@ export async function pageComplete(completeEvent: CompleteEvent) { : `${pageMeta.name}|${alias}`, detail: `alias to: ${pageMeta.name}`, type: "page", + cssClass, }); } } @@ -167,6 +169,7 @@ export async function pageComplete(completeEvent: CompleteEvent) { ? "Linked but not created" : undefined, type: "page", + cssClass, }); } else { // A markdown link []() @@ -182,9 +185,11 @@ export async function pageComplete(completeEvent: CompleteEvent) { } completions.push({ label: labelText, + displayLabel: namePrefix + labelText, boost: boost, apply: labelText.includes(" ") ? "<" + labelText + ">" : labelText, type: "page", + cssClass, }); } return completions; diff --git a/plugs/markdown/markdown_render.ts b/plugs/markdown/markdown_render.ts index 8d5fec72..2f936e38 100644 --- a/plugs/markdown/markdown_render.ts +++ b/plugs/markdown/markdown_render.ts @@ -577,6 +577,13 @@ export function renderMarkdownToHtml( const pageMeta = allPages.find((p) => pageRef.page === p.name); if (pageMeta) { t.body = [(pageMeta.pageDecoration?.prefix ?? "") + t.body]; + if (pageMeta.pageDecoration?.cssClasses) { + t.attrs!.class += " sb-decorated-object " + + pageMeta.pageDecoration.cssClasses.join(" ").replaceAll( + /[^a-zA-Z0-9-_ ]/g, + "", + ); + } } } if (t.body.length === 0) { diff --git a/web/client.ts b/web/client.ts index d9558190..fdba5d29 100644 --- a/web/client.ts +++ b/web/client.ts @@ -1,4 +1,7 @@ -import type { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; +import type { + CompletionContext, + CompletionResult, +} from "@codemirror/autocomplete"; import type { Compartment } from "@codemirror/state"; import { EditorView } from "@codemirror/view"; import { syntaxTree } from "@codemirror/language"; @@ -42,7 +45,11 @@ import { createEditorState } from "./editor_state.ts"; import { MainUI } from "./editor_ui.tsx"; import { cleanPageRef } from "$sb/lib/resolve.ts"; import type { SpacePrimitives } from "$common/spaces/space_primitives.ts"; -import type { CodeWidgetButton, FileMeta, PageMeta } from "../plug-api/types.ts"; +import type { + CodeWidgetButton, + FileMeta, + PageMeta, +} from "../plug-api/types.ts"; import { DataStore } from "$lib/data/datastore.ts"; import { IndexedDBKvPrimitives } from "$lib/data/indexeddb_kv_primitives.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; @@ -1100,6 +1107,15 @@ export class Client { // Nothing in the store, revert to default enrichedMeta = doc.meta; } + + const bodyEl = this.parent.parentElement; + if (bodyEl) { + bodyEl.removeAttribute("class"); + if (enrichedMeta.pageDecoration.cssClasses) { + bodyEl.className = enrichedMeta.pageDecoration.cssClasses.join(" ") + .replaceAll(/[^a-zA-Z0-9-_ ]/g, ""); + } + } this.ui.viewDispatch({ type: "update-current-page-meta", meta: enrichedMeta, diff --git a/web/cm_plugins/wiki_link.ts b/web/cm_plugins/wiki_link.ts index f457ae45..dbe65e75 100644 --- a/web/cm_plugins/wiki_link.ts +++ b/web/cm_plugins/wiki_link.ts @@ -77,6 +77,18 @@ export function cleanWikiLinkPlugin(client: Client) { const linkText = alias || ((pageMeta?.pageDecoration?.prefix ?? "") + cleanLinkText); + let cssClass = fileExists + ? "sb-wiki-link-page" + : "sb-wiki-link-page-missing"; + + if (pageMeta?.pageDecoration?.cssClasses) { + cssClass += " sb-decorated-object " + + pageMeta.pageDecoration.cssClasses.join(" ").replaceAll( + /[^a-zA-Z0-9-_ ]/g, + "", + ); + } + // And replace it with a widget widgets.push( Decoration.replace({ @@ -87,9 +99,7 @@ export function cleanWikiLinkPlugin(client: Client) { ? `Navigate to ${encodePageRef(pageRef)}` : `Create ${pageRef.page}`, href: `/${encodePageRef(pageRef)}`, - cssClass: fileExists - ? "sb-wiki-link-page" - : "sb-wiki-link-page-missing", + cssClass, from, callback: (e) => { if (e.altKey) { diff --git a/web/components/filter.tsx b/web/components/filter.tsx index f70b5ebc..c651cae1 100644 --- a/web/components/filter.tsx +++ b/web/components/filter.tsx @@ -197,9 +197,10 @@ export function FilterList({
{ if (selectedOption !== idx) { setSelectionOption(idx); diff --git a/web/components/page_navigator.tsx b/web/components/page_navigator.tsx index 3a5a3c20..44978fed 100644 --- a/web/components/page_navigator.tsx +++ b/web/components/page_navigator.tsx @@ -1,6 +1,9 @@ import { FilterList } from "./filter.tsx"; import type { FilterOption } from "$lib/web.ts"; -import type { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; +import type { + CompletionContext, + CompletionResult, +} from "@codemirror/autocomplete"; import type { PageMeta } from "../../plug-api/types.ts"; import { tagRegex as mdTagRegex } from "$common/markdown_parser/parser.ts"; @@ -42,6 +45,8 @@ export function PageNavigator({ // ... then we put it all the way to the end orderId = Infinity; } + const cssClass = (pageMeta.pageDecoration?.cssClasses || []).join(" ") + .replaceAll(/[^a-zA-Z0-9-_ ]/g, ""); if (mode === "page") { // Special behavior for regular pages @@ -66,6 +71,7 @@ export function PageNavigator({ description, orderId: orderId, hint: pageMeta._isBrokenLink ? "Create page" : undefined, + cssClass, }); } else if (mode === "meta") { // Special behavior for #template and #meta pages @@ -81,6 +87,7 @@ export function PageNavigator({ description: pageMeta.name, hint: pageMeta.tags![0], orderId: orderId, + cssClass, }); } else { // all // In mode "all" just show the full path and all tags @@ -94,6 +101,7 @@ export function PageNavigator({ name: pageMeta.name, description, orderId: orderId, + cssClass, }); } } diff --git a/web/components/top_bar.tsx b/web/components/top_bar.tsx index d082ec0d..33c8c4f4 100644 --- a/web/components/top_bar.tsx +++ b/web/components/top_bar.tsx @@ -33,6 +33,7 @@ export function TopBar({ onClick, rhs, pageNamePrefix, + cssClass, }: { pageName?: string; unsavedChanges: boolean; @@ -49,6 +50,7 @@ export function TopBar({ lhs?: ComponentChildren; rhs?: ComponentChildren; pageNamePrefix?: string; + cssClass?: string; }) { return (
{pageNamePrefix}
{!!viewState.panels.lhs.mode && ( diff --git a/website/Object Decorators.md b/website/Object Decorators.md index 57128897..93e3a63d 100644 --- a/website/Object Decorators.md +++ b/website/Object Decorators.md @@ -29,11 +29,12 @@ A few things of note: * `alwaysTen: '10'` (attaches an attribute named `alwaysTen` with the numeric value `10` to all objects matching the `where` clause) * `alwaysTrue: 'true'` (same as `alwaysTen` but with a boolean value) * `fullName: 'firstName + " " + lastName'` (attaches a `fullName` attribute that concatenates the `firstName` and `lastName` attributes with a space in between) - * `nameLength: 'count(name)'` (attaches an attribute `nameLength` with the string length of `name` — not particularly useful, but to demonstrate you can call [[Functions]] here too) + * `nameLength: 'count(name)'` (attaches an attribute `nameLength` with the string length of `name` — not particularly useful, but to demonstrate you can call [[Functions]] here too). ## Rules -A few rules to keep things civil: +A few rules and best practices to keep things civil: +* It is recommended to _always filter based on `tag`_ (so by adding e.g. `tag = "page"` to your `where` clause) to limit the radius of impact. Otherwise you may accidentally apply new attributes of all your [[Objects]] (items, tasks, pages, etc.). * Dynamic attributes _cannot override already existing attributes_. If the object already has an attribute with the same name, this value will be kept as is. * For performance reasons, all expressions (both filter and value expressions) need to be _synchronously evaluatable_. * Generally, this means they need to be “simple expressions” that require no expensive calls. diff --git a/website/Page Decorations.md b/website/Page Decorations.md index 6d2e8cfe..c36b9108 100644 --- a/website/Page Decorations.md +++ b/website/Page Decorations.md @@ -1,18 +1,46 @@ --- -pageDecoration.prefix: "🎄 " -pageDecoration.disableTOC: true +pageDecoration: + prefix: "🎄 " + disableTOC: true + cssClasses: + - christmas-decoration --- -Page decorations allow you to “decorate” pages in various ways. +Page decorations allow you to “decorate” pages in various fun ways. > **warning** Warning > This feature is still experimental and may change in the (near) future. - + # Supported decorations * `prefix`: A (visual) string prefix (often an emoji) to add to all page names. This prefix will appear in the top bar as well as in (live preview) links to this page. For example, the name of this page is actually “Page Decorations”, but when you link to it, you’ll see it’s prefixed with a 🎄: [[Page Decorations]] +* `cssClasses`: (list of strings) Attaches one or more CSS classes the page's `` tag, wiki links, auto complete items and [[Page Picker]] entries for more advanced styling through a [[Space Style]] (see below for an example for this page). * `hide` When this is set to `true`, the page will not be shown in [[Page Picker]], [[Meta Picker]], or suggested for completion of [[Links]]. It will otherwise behave as normal - will be [[Plugs/Index|indexed]] and found in [[Live Queries]]. The page can be opened through [[All Pages Picker]], or linked normally when the full name is typed out without completion. * `disableTOC` (not technically built-in, but a feature of the [[^Library/Core/Widget/Table of Contents]] widget): disable the [[Table of Contents]] for this particular page. -There are two ways to apply decorations to pages: +An example of using `cssClasses` on this page using [[Space Style]] (note the `pageDecoration.cssClasses` in this page’s [[Frontmatter]]): +```space-style + +/* Style page links */ +a.christmas-decoration { + background-color: #b4e46e; +} + +/* Style main editor components */ +body.christmas-decoration #sb-top { + background-color: #b4e46e; +} + +/* Style auto complete items */ +.cm-tooltip-autocomplete li.christmas-decoration { + background-color: #b4e46e; +} + +/* Style page picker item */ +.sb-result-list .sb-option.christmas-decoration { + background-color: #b4e46e; +} +``` + +There are two ways to _apply_ decorations to pages. # With [[Frontmatter]] directly This is demonstrated in the [[Frontmatter]] at the top of this page, by using the special `pageDecoration` attribute. This is how we get the fancy tree in front of the page name. Sweet. diff --git a/website/SETTINGS.md b/website/SETTINGS.md index b932c0db..59bee7a2 100644 --- a/website/SETTINGS.md +++ b/website/SETTINGS.md @@ -60,13 +60,13 @@ shortcuts: # Object decorators, see the "Page Decorators" page for more info objectDecorators: -- where: 'tags = "plug"' +- where: 'tag = "page" and tags = "plug"' attributes: pageDecoration.prefix: "'🔌 '" - where: 'tag = "human"' attributes: fullName: 'firstName + " " + lastName' -- where: 'tags = "notoc"' +- where: 'tag = "page" and tags = "notoc"' attributes: pageDecoration.disableTOC: "true"