mirror of
https://github.com/lumargh/better-bujo.git
synced 2026-07-22 07:39:31 +00:00
Add time event style setting, hand-drawn cancelled strikethrough
This commit is contained in:
parent
daed00bed3
commit
2182b528e3
5 changed files with 188 additions and 78 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
144
src/main.ts
144
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<void>();
|
||||
|
||||
// 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<Decoration>();
|
||||
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<Decoration>();
|
||||
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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
115
styles.css
115
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);
|
||||
}
|
||||
|
||||
/* ===========================================================================
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue