diff --git a/manifest.json b/manifest.json index cdc8b7b..71baa17 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "better-bujo", "name": "Better Bujo", - "version": "0.4.0", + "version": "0.4.1", "minAppVersion": "1.5.7", "description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.", "author": "Sebastien", diff --git a/package.json b/package.json index 2525c78..b503e23 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "better-bujo", - "version": "0.4.0", + "version": "0.4.1", "description": "Render bullet-journal markers (events, migration, future log, emotions) instead of checkboxes.", "main": "main.js", "type": "module", diff --git a/src/main.ts b/src/main.ts index c56bf7e..7bb7ea1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import { App, Editor, MarkdownView, Menu, Plugin, PluginSettingTab, Setting } from 'obsidian'; -import { RangeSetBuilder } from '@codemirror/state'; +import { RangeSetBuilder, StateEffect } from '@codemirror/state'; import { Decoration, DecorationSet, @@ -131,17 +131,19 @@ const emotionExtension = ViewPlugin.fromClass(EmotionView, { decorations: (value) => value.decorations, }); -// ---- Time-based events: `- [9:30 AM] …` → a clock on the bullet + the time. -- +// ---- Time-based events: `- [9:30 AM] …` ---- // Group 1: the bullet lead. Group 2: the time inside the brackets. const TIME_EVENT_LINE = /^(\s*(?:[-*+]|\d+[.)])\s+)\[(\d{1,2}:\d{2}\s*[AaPp][Mm])\]/; // The same time bracket, anchored to the start of a list item's rendered text. const TIME_EVENT_TEXT = /^\[(\d{1,2}:\d{2}\s*[AaPp][Mm])\]/; -const clockLine = Decoration.line({ class: 'bb-clock-line' }); +// Dispatched to open editors when the timeEventStyle setting changes so the +// decorations rebuild without waiting for a doc/viewport event. +const refreshClockEffect = StateEffect.define(); -// Live Preview: replace the `[9:30 AM]` source with just the time; the clock -// glyph itself is drawn on the list bullet by styles.css. +// Live Preview: replace `[9:30 AM]` with just the time text when the style +// removes the brackets; the bullet glyph is drawn by styles.css. class ClockWidget extends WidgetType { constructor(private readonly time: string) { super(); @@ -156,59 +158,77 @@ class ClockWidget extends WidgetType { } } -class ClockView implements PluginValue { - decorations: DecorationSet; +function makeClockExtension(getStyle: () => TimeEventStyle) { + class ClockView implements PluginValue { + decorations: DecorationSet; - constructor(view: EditorView) { - this.decorations = this.build(view); - } - - update(update: ViewUpdate): void { - if (update.docChanged || update.viewportChanged || update.selectionSet) { - this.decorations = this.build(update.view); + constructor(view: EditorView) { + this.decorations = this.build(view); } - } - private build(view: EditorView): DecorationSet { - const builder = new RangeSetBuilder(); - const { selection } = view.state; - for (const { from, to } of view.visibleRanges) { - let pos = from; - while (pos <= to) { - const line = view.state.doc.lineAt(pos); - const match = TIME_EVENT_LINE.exec(line.text); - if (match) { - const time = match[2] ?? ''; - const start = line.from + (match[1] ?? '').length; - const end = start + time.length + 2; // '[' + time + ']' - // Reveal the raw source while editing on the brackets. - const editing = selection.ranges.some((r) => r.from <= end && r.to >= start); - if (!editing) { - builder.add(line.from, line.from, clockLine); - builder.add(start, end, Decoration.replace({ widget: new ClockWidget(time) })); - } - } - pos = line.to + 1; + update(update: ViewUpdate): void { + if ( + update.docChanged || + update.viewportChanged || + update.selectionSet || + update.transactions.some((tr) => tr.effects.some((e) => e.is(refreshClockEffect))) + ) { + this.decorations = this.build(update.view); } } - return builder.finish(); + + private build(view: EditorView): DecorationSet { + const style = getStyle(); + const builder = new RangeSetBuilder(); + const { selection } = view.state; + for (const { from, to } of view.visibleRanges) { + let pos = from; + while (pos <= to) { + const line = view.state.doc.lineAt(pos); + const match = TIME_EVENT_LINE.exec(line.text); + if (match) { + const time = match[2] ?? ''; + const start = line.from + (match[1] ?? '').length; + const end = start + time.length + 2; // '[' + time + ']' + if (style === 'default') { + // Keep brackets visible; highlight with a mark span. + builder.add(start, end, Decoration.mark({ class: 'bb-clock-bracket' })); + } else { + // Reveal source while the cursor is on the brackets. + const editing = selection.ranges.some((r) => r.from <= end && r.to >= start); + if (!editing) { + const lineClass = + style === 'circle' + ? 'bb-clock-line-circle' + : `bb-clock-line-${/p/i.test(time) ? 'pm' : 'am'}`; + builder.add(line.from, line.from, Decoration.line({ class: lineClass })); + builder.add(start, end, Decoration.replace({ widget: new ClockWidget(time) })); + } + } + } + pos = line.to + 1; + } + } + return builder.finish(); + } } + return ViewPlugin.fromClass(ClockView, { decorations: (v) => v.decorations }); } -const clockExtension = ViewPlugin.fromClass(ClockView, { - decorations: (value) => value.decorations, -}); +type TimeEventStyle = 'default' | 'ampm' | 'circle'; interface BetterBujoSettings { strikeDoneTasks: boolean; dottedGrid: boolean; gridSpacing: number; + timeEventStyle: TimeEventStyle; } const DEFAULT_SETTINGS: BetterBujoSettings = { strikeDoneTasks: false, dottedGrid: false, gridSpacing: 20, + timeEventStyle: 'default', }; export default class BetterBujoPlugin extends Plugin { @@ -299,8 +319,7 @@ export default class BetterBujoPlugin extends Plugin { p.insertBefore(marker, first); } - // Time-based events: tag `- [9:30 AM] …` items and drop the brackets - // so styles.css can draw a clock on the bullet, leaving the time text. + // Time-based events: render `- [9:30 AM] …` according to the setting. for (const li of Array.from(el.querySelectorAll('li:not(.task-list-item)'))) { const host = li.firstElementChild instanceof HTMLParagraphElement ? li.firstElementChild : li; let node: ChildNode | null = host.firstChild; @@ -312,13 +331,28 @@ export default class BetterBujoPlugin extends Plugin { if (!node || !match) { continue; } - li.addClass('bb-clock'); - node.textContent = text.replace(TIME_EVENT_TEXT, match[1] ?? ''); + const time = match[1] ?? ''; + const full = match[0]; + if (this.settings.timeEventStyle === 'default') { + // Keep brackets; wrap them in a highlight span. + node.textContent = text.slice(full.length); + const span = li.ownerDocument.createElement('span'); + span.className = 'bb-clock-bracket'; + span.textContent = full; + host.insertBefore(span, node); + } else { + li.addClass( + this.settings.timeEventStyle === 'circle' + ? 'bb-clock-circle' + : /p/i.test(time) ? 'bb-clock-pm' : 'bb-clock-am' + ); + node.textContent = text.replace(TIME_EVENT_TEXT, time); + } } }); // Live Preview / Source mode equivalents. - this.registerEditorExtension([emotionExtension, clockExtension]); + this.registerEditorExtension([emotionExtension, makeClockExtension(() => this.settings.timeEventStyle)]); // A command per bullet type (so each can take a hotkey) plus a // right-click submenu — both rewrite the selected line(s) to that type. @@ -358,6 +392,13 @@ export default class BetterBujoPlugin extends Plugin { for (const doc of this.styledDocs) { this.styleDoc(doc); } + // Force live-preview clock decorations to rebuild in all open editors. + this.app.workspace.iterateAllLeaves((leaf) => { + if (leaf.view instanceof MarkdownView) { + const cm = (leaf.view.editor as unknown as { cm: EditorView }).cm; + cm.dispatch({ effects: refreshClockEffect.of(undefined) }); + } + }); } private styleDoc(doc: Document): void { @@ -498,5 +539,20 @@ class BetterBujoSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); }) ); + + new Setting(containerEl) + .setName('Time event style') + .setDesc('How to render time bullets like "- [9:30 am] …".') + .addDropdown((drop) => + drop + .addOption('default', 'Dash — show [time] highlighted') + .addOption('ampm', 'Am/pm label on bullet') + .addOption('circle', 'Circle (like an event)') + .setValue(this.plugin.settings.timeEventStyle) + .onChange(async (value) => { + this.plugin.settings.timeEventStyle = value as TimeEventStyle; + await this.plugin.saveSettings(); + }) + ); } } diff --git a/styles.css b/styles.css index 769c292..96e9b71 100644 --- a/styles.css +++ b/styles.css @@ -14,8 +14,7 @@ * - [O] event done → ● (same size as ○; click cycles back) * - [-] cancelled → •, marker and whole line struck through, faint * - … plain list item → – (en dash, not a bullet) - * - [9:30 AM] … time event → clock (hand-drawn clock on the bullet; the - * time stays as text, brackets dropped) + * - [9:30 AM] … time event → configurable: dash/AM-PM/circle * ~ … emotion/thought → styled line (the `~` glyph stays as typed) * * Strategy: never remove Obsidian's native marker elements — restyle them in @@ -43,11 +42,6 @@ done ✗, so the slash is literally the ✗'s first stroke at the same angle. */ --bb-stroke-w: max(1.5px, calc(var(--checkbox-size) * 0.1)); --bb-stroke-h: calc(var(--checkbox-size) * 0.62); - /* Hand-drawn clock face for time-based events (`- [9:30 AM] …`); used as a - mask so it takes --bb-marker-color, and roughened further by --bb-rough. - The face is a wobbly path with uneven radii that overshoots where it - closes (the way a penned circle does), and the hands are slightly skew. */ - --bb-clock-svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3.3C16.8 3 21.2 7.6 21 12.2C20.8 16.9 16.5 21.3 11.8 21C7 20.7 2.8 16.4 3 11.6C3.2 7.1 7.4 3.5 12.7 3.1'/%3E%3Cpath d='M12.1 12.2L11.7 7.4'/%3E%3Cpath d='M12.1 12.2L15.7 13.8'/%3E%3C/svg%3E"); /* Wet-ink bleed: a tight dark halo plus a soft outer one and a faint bottom-right pooling, so each glyph reads like ink soaked into paper rather than a crisp printed mark. Reused by every marker below. */ @@ -308,17 +302,36 @@ out an irrelevant task on paper. Mirrors Obsidian's native done styling, which targets these same elements. The marker's ::after is absolutely positioned, so the line's text-decoration doesn't propagate into it — the - strike on the • is applied explicitly below. */ + strike across the full item is drawn via ::before instead, so the same + hand-drawn filter can be applied. */ .better-bujo .markdown-rendered li[data-task="-"], .better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="-"] { - text-decoration: line-through; + text-decoration: none; color: var(--bb-done-color); + position: relative; +} + +/* Hand-drawn strikethrough: a thin bar absolutely centred on the line, warped + by the same rough filter as every other glyph. `rotate(-0.6deg)` gives a + barely-perceptible descending tilt, the way a hand-ruled line would. */ +.better-bujo .markdown-rendered li[data-task="-"]::before, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-task-line[data-task="-"]::before { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 50%; + height: max(2px, 0.09em); + transform: translateY(-50%) rotate(-0.6deg); + background-color: currentColor; + filter: var(--bb-rough); + pointer-events: none; + z-index: 1; } .better-bujo li[data-task="-"] > input.task-list-item-checkbox::after, .better-bujo li[data-task="-"] > p > input.task-list-item-checkbox::after, .better-bujo .HyperMD-task-line[data-task="-"] input.task-list-item-checkbox::after { - text-decoration: line-through; color: var(--bb-done-color); } @@ -395,34 +408,74 @@ } /* =========================================================================== - * Time-based events ("- [9:30 AM] …"). The plugin tags the item (.bb-clock in - * Reading mode, .bb-clock-line in Live Preview) and drops the brackets; here we - * swap the dash on the bullet for a hand-drawn clock face. `inset: 0; margin: - * auto` centers it in the same bullet box the dash used. + * Time-based events ("- [9:30 AM] …"). + * + * Three rendering styles, chosen in settings: + * + * default — dash bullet unchanged; [time] wrapped in .bb-clock-bracket for + * a muted highlight, brackets kept visible. + * ampm — brackets stripped; bullet replaced with a small "AM"/"PM" label + * (classes bb-clock-am / bb-clock-pm / bb-clock-line-am / -pm). + * circle — brackets stripped; bullet replaced with an event-style ring + * (classes bb-clock-circle / bb-clock-line-circle). + * + * `inset: 0; margin: auto` centers the replacement in the bullet box used by + * the dash, so it aligns with every other marker. * ======================================================================== */ -.better-bujo .markdown-rendered li.bb-clock .list-bullet::after, -.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line .list-bullet::after { +/* "default" style: muted colour on the [time] bracket span. */ +.better-bujo .bb-clock-bracket { + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +/* "ampm" style: shared layout for the AM/PM text label. */ +.better-bujo .markdown-rendered li.bb-clock-am .list-bullet::after, +.better-bujo .markdown-rendered li.bb-clock-pm .list-bullet::after, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-am .list-bullet::after, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-pm .list-bullet::after { + position: absolute; + inset: 0; + margin: auto; + display: flex; + align-items: center; + justify-content: center; + width: var(--checkbox-size); + height: var(--checkbox-size); + font-size: calc(var(--checkbox-size) * 0.34); + font-weight: 700; + letter-spacing: -0.02em; + color: var(--bb-marker-color); + transform: rotate(-4deg); + filter: var(--bb-rough); +} + +.better-bujo .markdown-rendered li.bb-clock-am .list-bullet::after, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-am .list-bullet::after { + content: "AM"; +} + +.better-bujo .markdown-rendered li.bb-clock-pm .list-bullet::after, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-pm .list-bullet::after { + content: "PM"; +} + +/* "circle" style: wobbly hand-drawn ring, matching the event ○ proportions. */ +.better-bujo .markdown-rendered li.bb-clock-circle .list-bullet::after, +.better-bujo .markdown-source-view.mod-cm6 .HyperMD-list-line.bb-clock-line-circle .list-bullet::after { content: ""; position: absolute; inset: 0; margin: auto; - width: calc(var(--checkbox-size) * 0.82); - height: calc(var(--checkbox-size) * 0.82); - border: none; - border-radius: 0; - background-color: var(--bb-marker-color); - -webkit-mask-image: var(--bb-clock-svg); - mask-image: var(--bb-clock-svg); - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - -webkit-mask-size: contain; - mask-size: contain; - transform: rotate(-4deg); + box-sizing: border-box; + width: calc(var(--checkbox-size) * 0.5); + height: calc(var(--checkbox-size) * 0.5); + background-color: transparent; + border: max(1px, calc(var(--checkbox-size) * 0.08)) solid var(--bb-marker-color); + border-radius: 47% 53% 52% 48% / 53% 47% 53% 47%; + transform: rotate(-8deg); filter: var(--bb-rough); - text-shadow: none; + box-shadow: var(--bb-ink-shadow); } /* =========================================================================== diff --git a/versions.json b/versions.json index 5ba6a4b..8e864fd 100644 --- a/versions.json +++ b/versions.json @@ -2,5 +2,6 @@ "0.1.0": "1.0.0", "0.2.0": "1.5.7", "0.3.0": "1.5.7", - "0.4.0": "1.5.7" + "0.4.0": "1.5.7", + "0.4.1": "1.5.7" } \ No newline at end of file