Skip to content
Merged
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ If the rule does not apply, paste the exact note path into **Path tester** and c

*All Markdown files mode protects every Markdown note unless an exclude rule matches.*

The welcome window is shown once for the current onboarding version and provides a shortcut to the settings page.
The welcome window is shown only after a new installation. Plugin updates, re-enabling, and Obsidian restarts do not show it again. It provides a shortcut to the settings page.

## Installation

Expand Down Expand Up @@ -245,6 +245,8 @@ The settings screen is organized around the main workflow:
- **Path tester** explains how a source resolves and whether the resulting note is protected.
- **Advanced** contains collapsible `Matching` and `Debug flags` sections.

Each section title is contained in its single section card; the settings UI does not add a second heading or outer frame around these cards. Top-level cards stay aligned with compact spacing, with extra separation before **Path rules** to distinguish configuration from rule management.

### Path rules

Each row contains:
Expand Down Expand Up @@ -282,6 +284,8 @@ For a system folder, the tester shows the resolved folder and asks for a specifi

On narrow phone and tablet settings panes, the interface switches to a stacked layout so rule controls, descriptions, resolved paths, and errors remain readable. Rules created from a system path on desktop continue to work from their saved vault-relative paths on mobile.

Changing a rule between `Include` and `Exclude` updates the existing row in place, so the mobile system picker closes normally after a selection.

### Advanced

The `Matching` and `Debug flags` sections are collapsed by default and expand inline. Their summaries show the current state, and their individual controls remain discoverable through settings search. Open `Matching` to switch between prefix and glob matching or to change case sensitivity. Debug options are intended only for diagnosing rule behavior.
Expand Down
13 changes: 8 additions & 5 deletions docs/PROJECT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ High-level modules:
- `src/settings-path-tester.ts`
- Path tester section rendering
- `src/settings-welcome.ts`
- Versioned onboarding modal and best-effort settings opening helper
- First-install welcome modal and best-effort settings opening helper
- `src/source-input-limits.ts`
- Source-input length policy, overflow handling, and display-safe truncation helpers
- `src/constants.ts`
Expand Down Expand Up @@ -240,17 +240,19 @@ UI module split:
- `src/settings-focus.ts` owns stable focus capture, restoration, and first-control focus.
- `src/settings-ui-state.ts` owns the pure summary/warning calculation used by the rules section.
- `src/settings-path-tester.ts` owns the path tester section.
- `src/settings-welcome.ts` owns the versioned onboarding modal shown for undismissed onboarding versions.
- `src/settings-welcome.ts` owns the first-install welcome modal.
- `src/source-input-limits.ts` owns accepted source lengths, overflow state, and display truncation.
- `src/rule-diagnostics.ts` provides pure helpers used by settings UI (rule diagnostics + path tester computations).

- Welcome modal:
- shown only when `dismissedWelcomeVersion < WELCOME_VERSION`
- dismissing or using `Open settings` saves the current onboarding version
- shown only when plugin data is absent on load; existing data marks an update or restart
- updates, re-enabling, and Obsidian restarts do not trigger it
- buttons, Escape, and the close control save the current dismissal version through one idempotent path
- action buttons use separated 44 px targets and inset focus indicators to avoid visual overlap
- Settings layout keeps the header, combined Enabled/mode card, Path rules, and Path tester workflow permanently visible. `Matching` and `Debug flags` remain inline collapsible sections with ephemeral open state in both renderers.
- On Obsidian 1.13+, `Read-only behavior` is one custom declarative item with `Enabled` and `Mode` search aliases, preserving the legacy two-card composition instead of allowing the host to split those controls into separate blocks.
- Obsidian 1.13+ indexes the two Advanced sections and exposes their four control names as search aliases; custom `render` callbacks preserve the plugin-owned accordion controls and their persistence side effects.
- Obsidian 1.13+ renders the complete settings stack through one custom item inside one heading-free group. The plugin therefore owns every inter-card gap directly; Obsidian cannot insert declarative section spacing between Mode, Path rules, Path tester, and Advanced. The scoped wrapper reset removes host borders/backgrounds, all cards remain full-width, and the stack uses the compact `--size-4-2` gap on desktop and narrow layouts. Path rules adds one extra `--size-4-2` top margin, making only the Mode-to-Path-rules separation twice the base gap.
- The Advanced declarative item exposes both section names and all four control names as search aliases; its custom `render` callback preserves the plugin-owned accordion controls and their persistence side effects.
- Obsidian versions before 1.13 use the legacy `display()` path, so the supported runtime baseline remains 1.10.3.
- Header card:
- title `Read Only View`
Expand All @@ -277,6 +279,7 @@ UI module split:
- advanced rows show their resolved vault path or a specific inline error without rewriting the active input
- all-Markdown mode visually marks every include row as inactive, excludes includes from active counts and diagnostics, and preserves each include rule's persisted enabled state
- exclude rows remain active in all-Markdown mode unless individually disabled
- changing a row between include and exclude updates it in place; the type selector is not recreated or programmatically refocused, preventing the native mobile picker from reopening
- add-rule button
- zero rules is a valid editor state; deleting the final row does not create a placeholder or empty-line diagnostic
- inline syntax help and README link
Expand Down
4 changes: 3 additions & 1 deletion docs/RELEASE_QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ Concise manual QA checklist for the next plugin release. Use the detailed releas

## Welcome, keyboard, and focus

- With an undismissed onboarding version, confirm the welcome modal appears once and **Open settings** opens this plugin's settings page.
- With plugin data absent, confirm the welcome modal appears and **Open settings** opens this plugin's settings page.
- Close the welcome modal with Escape or its close control, reload the plugin, and confirm it does not reappear.
- Update or re-enable an existing installation and confirm the welcome modal does not appear.
- Tab through both mode buttons, rule-row controls, Path tester, and the **Matching** and **Debug flags** disclosure buttons.
- Confirm native `Enter`/`Space` activation works, focus indicators stay visible, and disclosure controls expose correct `aria-expanded` state.
- Add, delete, enable, disable, or edit a rule and confirm focus returns to the corresponding stable control after the UI rerenders.
Expand Down
21 changes: 12 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export default class ReadOnlyViewPlugin extends Plugin {
private compiledRuleMatcherKey = getCompiledRuleMatcherKey(this.settings);

async onload(): Promise<void> {
await this.loadSettings();
const isFreshInstall = await this.loadSettings();
this.registerEditorExtension(createEditorReadOnlyExtension({
shouldForceReadOnlyPath: (path) => this.shouldForceReadOnlyPath(path),
onReadOnlyInteraction: (info, reason) => {
Expand Down Expand Up @@ -89,7 +89,9 @@ export default class ReadOnlyViewPlugin extends Plugin {
this.addSettingTab(new ForceReadModeSettingTab(this.app, this));

await this.applyAllOpenMarkdownLeaves('onload');
maybeShowWelcomeModal(this.app, this, this.manifest.id);
if (isFreshInstall) {
maybeShowWelcomeModal(this.app, this, this.manifest.id);
}
}

onunload(): void {
Expand All @@ -108,10 +110,11 @@ export default class ReadOnlyViewPlugin extends Plugin {
}
}

async loadSettings(): Promise<void> {
async loadSettings(): Promise<boolean> {
const loaded: unknown = await this.loadData();
this.settings = mergeLoadedSettings(loaded);
this.rebuildCompiledRuleMatcher();
return loaded === null || loaded === undefined;
}

async saveSettings(): Promise<void> {
Expand Down
26 changes: 20 additions & 6 deletions src/settings-rule-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,15 +174,18 @@ function getRulesChangeReason(previous: RuleEditorUiState, next: RuleEditorUiSta
const excludeChanged = previous.excludeText !== next.excludeText;
const enabledChanged = previous.includeRuleEnabled.join() !== next.includeRuleEnabled.join()
|| previous.excludeRuleEnabled.join() !== next.excludeRuleEnabled.join();
if (enabledChanged) {
return 'settings-rule-enabled';
}
if (includeChanged && !excludeChanged) {
return 'settings-include-rules';
}
if (!includeChanged && excludeChanged) {
return 'settings-exclude-rules';
}
if (includeChanged && excludeChanged) {
return 'settings-path-rules';
}
if (enabledChanged) {
return 'settings-rule-enabled';
}
return 'settings-path-rules';
}

Expand Down Expand Up @@ -675,15 +678,16 @@ export function renderRuleEditor(options: RenderRuleEditorOptions): RuleEditorCo
excludeIndex++;
}
}
rowControllers.set(row.id, {
const controller: RuleRowController = {
row,
type: row.type,
indexWithinType,
enabled: row.enabled,
inactiveByMode,
messageEl,
resolution,
});
};
rowControllers.set(row.id, controller);

enabledEl.addEventListener('change', () => {
row.enabled = enabledEl.checked;
Expand All @@ -693,7 +697,17 @@ export function renderRuleEditor(options: RenderRuleEditorOptions): RuleEditorCo

typeSelectEl.addEventListener('change', () => {
row.type = typeSelectEl.value === 'exclude' ? 'exclude' : 'include';
renderRows();
const inactiveByMode = row.type === 'include' && !includeRulesActive;
controller.type = row.type;
controller.inactiveByMode = inactiveByMode;
inputEl.placeholder = row.type === 'include' ? 'projects/' : 'projects/drafts/';
inputEl.setAttr('aria-label', `${row.type === 'include' ? 'Include' : 'Exclude'} rule value`);
deleteButtonEl.setAttr('aria-label', `Delete ${row.type} rule`);
if (inactiveByMode) {
rowEl.addClass('is-inactive-by-mode');
} else {
rowEl.removeClass('is-inactive-by-mode');
}
syncRows(true);
});

Expand Down
Loading
Loading