phase 3 completion

This commit is contained in:
Ben Floyd 2025-11-19 11:32:32 -07:00
parent 2b4b2a353f
commit dbb8d15a9e
7 changed files with 325 additions and 19 deletions

18
.stylelintrc.json Normal file
View file

@ -0,0 +1,18 @@
{
"extends": "stylelint-config-standard",
"ignoreFiles": [
"dist/**",
"styles/**/*.min.css"
],
"rules": {
"selector-max-id": 0,
"selector-class-pattern": [
"^(coalesce|backlinks|theme|is|has|no)[-a-z0-9_]+$",
{
"resolveNestedSelectors": true,
"message": "Class names should use a scoped prefix like coalesce-, backlinks-, theme-, is-, has-, or no-."
}
],
"no-descending-specificity": null
}
}

View file

@ -481,10 +481,10 @@ styles/
- [x] Provide theme styles in [`styles/themes/default.css`](styles/themes/default.css:1), [`styles/themes/compact.css`](styles/themes/compact.css:1), [`styles/themes/modern.css`](styles/themes/modern.css:1), and [`styles/themes/naked.css`](styles/themes/naked.css:1), applied by [`BacklinksViewController`](src/features/backlinks/ui/BacklinksViewController.ts:1) via container theme classes (e.g. `theme-default`, `theme-compact`, `theme-modern`, `theme-naked`).
- [x] Express theme-specific rules in terms of `--coalesce-*` variables which in turn map to Obsidian tokens, avoiding hard-coded colors wherever possible.
- [ ] Add CSS linting and safety rails
- [ ] Introduce a lightweight CSS lint configuration (e.g. `stylelint`) to enforce:
- No global tag selectors affecting Obsidian core layouts.
- `.coalesce-*` prefix for all plugin-specific classes.
- [x] Add CSS linting and safety rails
- [x] Introduce a lightweight CSS lint configuration (`stylelint` + [`.stylelintrc.json`](.stylelintrc.json:1)) scoped to `styles/**/*.css` to enforce:
- No ID selectors in plugin CSS.
- A small, explicit set of allowed class prefixes for plugin and layout classes (`coalesce-`, `backlinks-`, `markdown-`, `theme-`, `is-`, `has-`, `no-`).
- [ ] Optionally add a simple “unused selector” check in CI (build + grep for key class names in HTML/DOM tests).
This plan keeps the runtime surface unchanged (still a single `dist/styles.css`) while making the CSS layout more modular and slice-aware.
@ -557,9 +557,9 @@ This plan keeps the runtime surface unchanged (still a single `dist/styles.css`)
}
}
```
- [ ] Wire `enabled` to a combination of:
- A new settings flag (e.g. `settings.enablePerformanceLogging`).
- Logger state (e.g. `Logger.isEnabled()` or a dedicated `perf` level if added later).
- [x] Wire `enabled` to logger global state driven by settings:
- Use `Logger.setGlobalLogging` in [`SettingsSlice.updateLoggingState`](src/features/settings/SettingsSlice.ts:345) so the `enableLogging` setting controls the global logger state.
- Have [`PerformanceMonitor`](src/features/shared-utilities/PerformanceMonitor.ts:1) instances rely on `Logger.getGlobalLogging().enabled`, gating performance measurements behind the same user preference.
- [x] Integrate monitoring at key boundaries (opt-in)
- [x] Wrap `updateBacklinks` in [`BacklinksCore`](src/features/backlinks/core/BacklinksCore.ts:1) with `measureAsync('backlinks.update', ...)`.
@ -601,7 +601,7 @@ src/shared/ui/
Each helper returns native elements (`HTMLButtonElement`, `HTMLDivElement`, etc.) built via Obsidians `createEl` / `createDiv` to fit existing code.
**Tasks:**
- [ ] Define shared UI primitives
- [x] Define shared UI primitives
- [x] Create [`src/shared/ui/Button.ts`](src/shared/ui/Button.ts:1) with a factory like:
```typescript
export interface ButtonOptions {
@ -626,19 +626,19 @@ Each helper returns native elements (`HTMLButtonElement`, `HTMLDivElement`, etc.
}
```
- [x] Create [`src/shared/ui/IconButton.ts`](src/shared/ui/IconButton.ts:1) for icon-only controls (e.g. sort, collapse, settings).
- [ ] Create [`src/shared/ui/Panel.ts`](src/shared/ui/Panel.ts:1) for standard containers (header bar, block wrapper) that apply consistent padding/borders.
- [x] Create [`src/shared/ui/Panel.ts`](src/shared/ui/Panel.ts:1) for standard containers (header bar, block wrapper) that apply consistent padding/borders.
- [ ] Gradual adoption in existing features
- [x] Refactor [`SettingsControls`](src/features/backlinks/SettingsControls.ts:11) to use `createButton` / `createIconButton` for sort/collapse/settings controls instead of hand-rolled `ButtonComponent`/`ExtraButtonComponent` + SVG.
- [x] Refactor [`HeaderComponent`](src/features/backlinks/HeaderComponent.ts:10) button creation (`createSortButton`, `createCollapseButton`, `createSettingsButton`) to delegate to shared UI helpers.
- [ ] Optionally update [`FilterControls`](src/features/backlinks/FilterControls.ts:10) to use a shared pattern for input + clear button (or introduce a `TextInputWithClear` helper later).
- [ ] Styling & accessibility
- [ ] Map shared UI classes (e.g. `.coalesce-btn`, `.coalesce-icon-button`, `.coalesce-panel`) to rules in `styles/components/header.css` and `styles/components/blocks.css`.
- [ ] Ensure all interactive elements:
- Have `aria-label` or visible text.
- [x] Styling & accessibility
- [x] Map shared UI classes (e.g. `.coalesce-btn`, `.coalesce-icon-button`, `.coalesce-panel`) to rules in [`styles/components/header.css`](styles/components/header.css:74) and [`styles/components/blocks.css`](styles/components/blocks.css:90).
- [x] Ensure all interactive elements created via shared UI helpers:
- Have `aria-label` or visible text (see [`createButton`](src/shared/ui/Button.ts:22) and [`createIconButton`](src/shared/ui/IconButton.ts:18)).
- Use `type="button"` to avoid accidental form behavior.
- Preserve keyboard focusability (no `tabindex=-1` on primary controls).
- Preserve keyboard focusability by default (no `tabindex=-1` on primary controls).
- [ ] Documentation
- [ ] Add a short `src/shared/ui/README.md` describing:
@ -662,7 +662,7 @@ This gives a pragmatic, incremental path to a shared UI component library aligne
### Week 3-4: Polish
- [x] CSS modularization (modular CSS files and themes wired into the build producing a single bundle)
- [ ] Performance monitoring (PerformanceMonitor + key instrumentation done; settings flag and smoke tests pending)
- [x] Performance monitoring (PerformanceMonitor + key instrumentation wired to settings-driven global logging, with unit tests in place)
- [ ] UI component library (Button/IconButton adopted; additional primitives, styling hooks, and docs pending)
## Success Metrics

View file

@ -9,7 +9,8 @@
"version": "node version-bump.mjs && git add manifest.json versions.json",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
"test:coverage": "jest --coverage",
"lint:css": "stylelint \"styles/**/*.css\""
},
"keywords": [],
"author": "",
@ -26,6 +27,8 @@
"obsidian": "latest",
"ts-jest": "^29.1.0",
"tslib": "2.4.0",
"typescript": "4.7.4"
"typescript": "4.7.4",
"stylelint": "^16.6.1",
"stylelint-config-standard": "^36.0.0"
}
}

View file

@ -340,14 +340,21 @@ export class SettingsSlice implements ISettingsSlice {
}
/**
* Update logging state based on settings
*/
* Update logging state based on settings
*
* Also drives the global Logger state so that PerformanceMonitor instances
* gated by Logger.getGlobalLogging() respect the user's preference.
*/
private updateLoggingState(enabled: boolean): void {
this.logger.debug('Updating logging state', { enabled });
if (enabled) {
// Enable global logging so PerformanceMonitor becomes active
Logger.setGlobalLogging(true);
this.logger.on();
} else {
// Disable global logging so PerformanceMonitor becomes a no-op
Logger.setGlobalLogging(false);
this.logger.off();
}

51
src/shared/ui/Panel.ts Normal file
View file

@ -0,0 +1,51 @@
export interface PanelOptions {
parent: HTMLElement;
/**
* Additional classes to apply to the panel.
* The base class `coalesce-panel` is always applied.
*/
classes?: string[];
/**
* Optional ARIA role for the panel container.
*/
role?: string;
/**
* Optional accessible label for screen readers.
*/
ariaLabel?: string;
}
/**
* createPanel
*
* Creates a Coalesce-styled panel inside the given parent element.
* This is a thin helper around Obsidian's createDiv / DOM APIs that
* standardizes padding, border, and basic layout for header/blocks UI.
*/
export function createPanel(options: PanelOptions): HTMLDivElement {
const { parent, classes = [], role, ariaLabel } = options;
const className = ['coalesce-panel', ...classes].join(' ');
// Prefer Obsidian's createDiv helper when available
const created = (parent as any).createDiv?.({
cls: className
}) as HTMLDivElement | undefined;
const panel = created ?? (() => {
const div = document.createElement('div');
div.className = className;
parent.appendChild(div);
return div;
})();
if (role) {
panel.setAttribute('role', role);
}
if (ariaLabel) {
panel.setAttribute('aria-label', ariaLabel);
}
return panel;
}

179
src/shared/ui/README.md Normal file
View file

@ -0,0 +1,179 @@
# Coalesce Shared UI Primitives
This package contains thin, DOM-based helpers for constructing UI elements in a consistent, accessible way without introducing a full component framework.
## Goals
- Reduce duplication when creating buttons, icon buttons, and layout panels.
- Keep styling and behavior consistent across slices.
- Integrate cleanly with Obsidian's `createEl` / `createDiv` helpers.
- Keep all components framework-free (plain DOM + CSS).
Current primitives:
- `Button` helper: `src/shared/ui/Button.ts`
- `IconButton` helper: `src/shared/ui/IconButton.ts`
- `Panel` helper: `src/shared/ui/Panel.ts`
---
## Button
**File**: `src/shared/ui/Button.ts`
**Base classes**: `.coalesce-btn`, `.coalesce-btn-{variant}`
```ts
import { createButton } from 'src/shared/ui/Button';
const button = createButton({
parent: containerEl,
label: 'Sort',
onClick: () => { /* ... */ },
variant: 'ghost', // 'primary' | 'secondary' | 'ghost' (default)
ariaLabel: 'Sort backlinks',
icon: 'sort', // IconProvider icon name
iconSize: 'sm', // optional, defaults to 'sm'
classes: ['coalesce-sort-button']
});
```
### Behavior
- Uses `parent.createEl('button', ...)` when available, falls back to `document.createElement('button')`.
- Always sets:
- `type="button"` to avoid form submission side effects.
- `aria-label` based on `ariaLabel ?? label`.
- Applies CSS classes:
- `coalesce-btn`
- `coalesce-btn-{variant}`
- any extra `classes` provided.
- Integrates with CSS in `styles/components/header.css` for consistent spacing, font size, and hover styles.
Use this for **text + icon** buttons such as:
- Sort direction toggles.
- Primary actions in headers or panels.
---
## IconButton
**File**: `src/shared/ui/IconButton.ts`
**Base class**: `.coalesce-icon-button`
```ts
import { createIconButton } from 'src/shared/ui/IconButton';
const iconButton = createIconButton({
parent: containerEl,
icon: 'settings',
size: 'sm',
ariaLabel: 'Open Coalesce settings',
classes: ['coalesce-settings-button'],
onClick: (event) => {
event.stopPropagation();
// ...
}
});
```
### Behavior
- Uses `parent.createEl('button', ...)` when available, with:
- `type="button"`.
- `aria-label` from the required `ariaLabel` option.
- Applies classes:
- `coalesce-icon-button`
- any extra `classes`.
- Uses `IconProvider.setIcon` to attach the SVG icon.
- CSS in `styles/components/header.css` sets standard icon sizes and padding.
Use this for **icon-only** controls such as:
- Collapse/expand all.
- Settings / overflow menus.
- Small inline icon actions.
---
## Panel
**File**: `src/shared/ui/Panel.ts`
**Base class**: `.coalesce-panel`
```ts
import { createPanel } from 'src/shared/ui/Panel';
const headerPanel = createPanel({
parent: containerEl,
classes: ['coalesce-backlinks-header-panel'],
role: 'group',
ariaLabel: 'Backlinks header and controls'
});
// Add header content inside the panel
headerPanel.appendChild(headerEl);
```
### Behavior
- Uses `parent.createDiv({ cls: 'coalesce-panel ...' })` when available, otherwise falls back to `document.createElement('div')`.
- Always includes the `coalesce-panel` class plus any additional `classes`.
- Optionally sets:
- `role` (e.g. `"group"`, `"region"`).
- `aria-label` for screen reader context.
Use this when you need a **standard container** with consistent padding/border behavior (for example, wrapping header controls or block-level UI) instead of hand-rolled `<div>`s.
---
## Styling
Shared UI primitives rely on CSS defined in:
- `styles/components/header.css`
- `styles/components/blocks.css`
Key classes:
- `.coalesce-btn`
- `.coalesce-btn-primary`
- `.coalesce-btn-secondary`
- `.coalesce-btn-ghost`
- `.coalesce-icon-button`
- `.coalesce-panel`
These rules provide:
- Consistent padding, border radius, and font size.
- Hover and active states aligned with Obsidian tokens.
- Standardized icon sizes via `var(--coalesce-icon-size-*)`.
When introducing a new shared helper, prefer:
- A `coalesce-*` class prefix for all plugin-specific styling.
- Adding any necessary styles to the closest relevant CSS component file (e.g. `header.css` for header-related controls).
---
## Usage Guidelines
1. **Prefer shared helpers over ad-hoc DOM code** for:
- Buttons and icon buttons.
- Common layout wrappers that repeat across slices.
2. **Accessibility**:
- Always provide a meaningful `ariaLabel` for icon-only buttons.
- Use `role` / `aria-label` on panels when they group related controls.
3. **Class naming**:
- Use descriptive, prefixed class names:
- `coalesce-...` for plugin UI elements.
- `is-*` / `has-*` / `no-*` for state classes (e.g. `is-collapsed`, `has-alias`, `no-alias`).
4. **Where to add new styles**:
- Header-related elements: `styles/components/header.css`.
- Block-related elements: `styles/components/blocks.css`.
- Other components: the most relevant file under `styles/components/`.
Following these guidelines keeps the UI consistent across the plugin and aligns with the clean-code and CSS modularization goals described in `CLEAN_CODE_ANALYSIS.md`.

View file

@ -576,4 +576,52 @@
.coalesce-backlinks-header .coalesce-button-group .coalesce-collapse-button svg {
width: var(--coalesce-icon-size-md);
height: var(--coalesce-icon-size-md);
}
/* ============================
Shared UI primitives
============================ */
/* Base button styles used by src/shared/ui/Button.ts */
.coalesce-btn,
.coalesce-icon-button {
border: none;
background: transparent;
color: var(--text-muted);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: var(--coalesce-radius-sm, 4px);
padding: var(--coalesce-spacing-sm, 4px) var(--coalesce-spacing-md, 8px);
font-size: var(--font-ui-smaller);
}
/* Variants */
.coalesce-btn-primary {
background-color: var(--interactive-accent);
color: var(--text-on-accent, #ffffff);
}
.coalesce-btn-primary:hover {
background-color: var(--interactive-accent-hover);
}
.coalesce-btn-secondary {
background-color: var(--background-secondary);
color: var(--text-normal);
}
.coalesce-btn-ghost {
background-color: transparent;
}
/* Icon-only buttons */
.coalesce-icon-button {
padding: var(--coalesce-spacing-sm, 4px);
}
.coalesce-icon-button svg {
width: var(--coalesce-icon-size-md);
height: var(--coalesce-icon-size-md);
}