diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..ddbe8c6 --- /dev/null +++ b/.stylelintrc.json @@ -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 + } +} \ No newline at end of file diff --git a/CLEAN_CODE_ANALYSIS.md b/CLEAN_CODE_ANALYSIS.md index a5aad7b..5c25292 100644 --- a/CLEAN_CODE_ANALYSIS.md +++ b/CLEAN_CODE_ANALYSIS.md @@ -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 Obsidian’s `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 diff --git a/package.json b/package.json index 403909a..403a636 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/features/settings/SettingsSlice.ts b/src/features/settings/SettingsSlice.ts index 52aebb9..bff012f 100644 --- a/src/features/settings/SettingsSlice.ts +++ b/src/features/settings/SettingsSlice.ts @@ -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(); } diff --git a/src/shared/ui/Panel.ts b/src/shared/ui/Panel.ts new file mode 100644 index 0000000..9751b4a --- /dev/null +++ b/src/shared/ui/Panel.ts @@ -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; +} \ No newline at end of file diff --git a/src/shared/ui/README.md b/src/shared/ui/README.md new file mode 100644 index 0000000..a924126 --- /dev/null +++ b/src/shared/ui/README.md @@ -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 `
`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`. \ No newline at end of file diff --git a/styles/components/header.css b/styles/components/header.css index 01c18d1..3ca207b 100644 --- a/styles/components/header.css +++ b/styles/components/header.css @@ -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); } \ No newline at end of file