Compare commits

..

25 commits
1.3.0 ... main

Author SHA1 Message Date
Ben Floyd
ab2e58bd85 v1.3.4 2026-07-02 19:45:16 -06:00
Ben Floyd
a9cde02c78 streambar layout adjust 2026-07-02 19:42:58 -06:00
Ben Floyd
37eeb11247 layout adjust streams bar 2026-07-02 18:27:57 -06:00
Ben Floyd
f1afd09bda style adjust 2026-07-02 18:25:48 -06:00
Ben Floyd
f85a795dcc pill style for desktop 2026-07-02 16:25:52 -06:00
Ben Floyd
4e109b0798 desktop newline 2026-07-02 16:16:53 -06:00
Ben Floyd
d7ce138f91 remove bar style 2026-07-02 16:06:18 -06:00
Ben Floyd
24c52fa626 use chevron instead of ascii 2026-07-02 15:51:23 -06:00
Ben Floyd
a120018138 layout updates 2026-07-02 11:57:49 -06:00
Ben Floyd
ee989d755c fix comment 2026-07-02 11:49:01 -06:00
Ben Floyd
2766228af4 layout fix 2026-07-02 11:45:46 -06:00
Ben Floyd
4577611ce5 improve layout 2026-07-02 11:20:05 -06:00
Ben Floyd
8eebf31015 remove header spacing on pdf 2026-07-01 20:41:37 -06:00
Ben Floyd
335e05567f v1.3.3 2026-06-03 16:26:00 -06:00
Ben Floyd
cd83a82c58 add attestations 2026-06-03 16:25:27 -06:00
Ben Floyd
74cf520b4a add skill 2026-06-03 16:19:22 -06:00
Ben Floyd
c91b2a1460 v1.3.2 2026-06-03 16:13:17 -06:00
Ben Floyd
5dfdc808ea remove :has 2026-06-03 16:12:55 -06:00
Ben Floyd
6d0d067071 remove !important 2026-06-03 16:11:00 -06:00
Ben Floyd
3866e55779 no more builtin-modules 2026-06-03 16:09:04 -06:00
Ben Floyd
f6f92ce247 v1.3.1 2026-06-03 10:51:55 -06:00
Ben Floyd
654ba6f8f7 remove unused providesApi 2026-06-03 10:51:30 -06:00
Ben Floyd
ae909be484 date mismatch fix 2026-06-03 10:49:49 -06:00
Ben Floyd
5c489e3bb3 add adrs and functionality 2026-03-25 08:52:38 -06:00
Ben Floyd
748bb00644 no longer override pinned tab. ctrl click also open new tab 2026-03-23 07:05:35 -06:00
30 changed files with 577 additions and 286 deletions

View file

@ -0,0 +1,41 @@
---
name: managing-obsidian-plugin
description: Manages the development, refactoring, and maintenance of an Obsidian plugin coding project. Use when the user requests feature additions, bug fixes, UI improvements, or refactoring in the Obsidian plugin codebase.
---
# Managing Obsidian Plugin
## When to use this skill
- Adding new features or settings to the Obsidian plugin.
- Refactoring existing plugin components (e.g., UI, logic).
- Fixing bugs or testing plugin functionality.
- Managing Obsidian API interactions.
## Workflow
### 1. Plan & Analyze
- [ ] Read relevant workspace files to locate the affected components (e.g., `main.ts`, view files, or service classes).
- [ ] Check `manifest.json` for required plugin version bumps if deploying a new release.
- [ ] Identify dependencies between UI components, styles, and core logic.
### 2. Validate
- [ ] Validate changes against existing test suites using relevant testing scripts.
- [ ] Check if the changes necessitate an update to settings menus or serialized data structures.
### 3. Execute
- [ ] Apply code changes using progressive and isolated module updates.
- [ ] Run `npm run build` to verify compilation without errors.
## Instructions
- Keep interactions with the Obsidian API well-isolated and properly typed.
- Prioritize updating or creating unit tests alongside feature development to verify logic safely.
- If a build script fails, do not proceed blindly; troubleshoot the specific error output before continuing.
## Architecture
- We use a vertical slice architecture.
- Strict adherance to cleancode rules.
- Use the provided .agent/skills/obsidian-plugin/SKILL.md as the main source of truth for coding standards and best practices.
- Use ADRs to document design decisions in adr/. Refer to the ADRs folder for more information and ensure compliance with existing ADRs.
## Resources
- None

47
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,47 @@
name: Release Obsidian Plugin
on:
push:
tags:
- "*"
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Build plugin
run: npm run build
- name: Attest build provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: |
dist/main.js
dist/manifest.json
dist/styles.css
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
dist/main.js
dist/manifest.json
dist/styles.css
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

57
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,57 @@
# Contributing to Streams
Welcome! We gladly accept contributions from the community. If you'd like to help improve Streams, follow the steps below.
## How to Contribute
1. **Fork the repository** and clone it to your local machine.
2. **Install dependencies** by running `npm install`.
3. **Create a new branch** for your feature or bug fix (`git checkout -b feature/your-feature-name`).
4. **Make your changes**. You can use `npm run dev` to watch for changes and compile the plugin, then test them locally in your Obsidian vault.
5. **Commit your changes** with clear, descriptive commit messages.
6. **Push to your fork** and open a Pull Request against our `main` branch.
All Pull Requests will be reviewed and merged by the maintainer.
---
## Maintainer Guide: Releasing a New Version
*(The following instructions are for project maintainers only)*
The release process is fully automated via GitHub Actions. **You do not need to build the plugin or generate attestations manually.**
Whenever a new tag is pushed, a background GitHub Action will automatically:
- **Build the plugin** (`main.js`, `styles.css`) from source.
- **Generate cryptographic artifact attestations** for security and provenance.
- **Publish a GitHub Release** and attach the built artifacts.
Before triggering a release, **you must bump the version numbers**:
1. Run `npm run version` (or manually update `manifest.json` and `versions.json`).
2. Commit and push the changes to the `main` branch.
Once the versions are bumped, you can trigger the automated release using either the GitHub UI or the command line.
### Option 1: Using the GitHub Releases UI (Recommended)
This is the simplest method if you prefer a visual interface.
1. Navigate to the **Releases** page on your GitHub repository.
2. Click the **Draft a new release** button.
3. In the "Choose a tag" dropdown, type your new version tag (e.g., `1.0.6`) and click **Create new tag: 1.0.6 on publish**.
4. (Optional) Fill out the release title and notes.
5. Click **Publish release**.
> **Note:** As soon as you click publish, GitHub will trigger the Action in the background. Navigate to the **Actions** tab to watch it build, sign, and attach the files to your newly drafted release!
### Option 2: Using the Command Line (CLI)
If you prefer terminal commands, you can push the tag directly via Git.
1. Create a local tag for your new version:
```bash
git tag 1.0.6
```
2. Push the tag to GitHub:
```bash
git push origin main --tags
```
> **Note:** Pushing the tag automatically triggers the GitHub Action. The Action will build the assets and create the GitHub Release for you automatically. You can edit the release notes in the GitHub UI afterward if needed.

View file

@ -0,0 +1,63 @@
# ADR-0001: Leaf selection behavior for stream navigation
**Date:** 2026-03-25
**Status:** Accepted
**Affects:** `LeafSelectionService.ts`, `RibbonService.ts`, `OpenTodayStreamCommand.ts`
## Context
When a user clicks a ribbon icon (or runs a command) to open a stream's daily note, the plugin must decide *which* workspace tab (leaf) to open the file in. Obsidian's workspace has three areas:
- **Main editor area** (`workspace.rootSplit`) — the central pane(s) where notes are edited
- **Left sidebar** (`workspace.leftSplit`) — file explorer, bookmarks, etc.
- **Right sidebar** (`workspace.rightSplit`) — backlinks, outgoing links, etc.
Prior to this decision, the plugin had several issues:
1. **Pinned tabs were overwritten** — clicking a ribbon icon while a pinned tab was active would load the stream note into that pinned tab, violating user intent.
2. **New tabs were always created** — when the active tab couldn't be reused (e.g., it was pinned), the plugin created a brand-new tab rather than reusing an existing unpinned one, leading to tab proliferation.
3. **Side panes could be targeted** — the leaf selection logic didn't distinguish between main editor leaves and sidebar leaves, so a stream note could open inside a sidebar panel.
## Decision
### 1. Never navigate into pinned tabs
All leaf selection paths check `(leaf as any).pinned === true` before reusing a leaf. If the active leaf is pinned, it is skipped entirely.
### 2. Prefer reusing an existing unpinned tab over creating a new one
When the active leaf can't be used (pinned or in a side pane), `findNextUnpinnedLeaf()` scans the workspace for the first unpinned, eligible leaf in the main editor area. A new tab is only created as a last resort when no candidate is found.
### 3. Restrict all leaf selection to the main editor area
Every leaf candidate is checked via `leaf.getRoot() === app.workspace.rootSplit`. Leaves in left/right sidebars are never selected for navigation. This applies to:
- `findNextUnpinnedLeaf()` — only scans main editor leaves
- `reuseCurrentLeaf()` — won't reuse the active leaf if it's in a sidebar
- `selectOrCreateLeaf()` — same guard
- `selectLeafForFile()` — won't match existing leaves in sidebars
### 4. Ctrl/Cmd+click forces a new tab
Ribbon icon callbacks capture the `MouseEvent`. When `ctrlKey` (Windows/Linux) or `metaKey` (macOS) is held, a fresh leaf is created via `app.workspace.getLeaf('tab')` and passed as `targetLeaf`, bypassing all reuse logic. This matches standard Obsidian/browser conventions.
## Leaf selection priority
For a normal click, the selection order is:
1. **Active leaf** — if it's unpinned, in the main editor area, and passes any view-type filter
2. **Existing leaf with same file** — (for `selectLeafForFile` only) if already open in the main editor
3. **Next unpinned leaf** — first unpinned leaf found in the main editor area
4. **New tab** — created via `getLeaf('tab')` as a last resort
For Ctrl/Cmd+click:
1. **New tab** — always, via `getLeaf('tab')` passed as `targetLeaf`
## Consequences
- **Pinned tabs are always respected.** Users can pin a tab and know it won't be overwritten by ribbon navigation.
- **Tab count stays manageable.** Reusing unpinned tabs prevents accumulation of redundant tabs.
- **Sidebar integrity is preserved.** File explorer, backlinks, and other sidebar views are never replaced with stream content.
- **Ctrl+click provides an escape hatch.** Users who *do* want a new tab can explicitly request one.
- **Relies on internal API.** The `pinned` property on `WorkspaceLeaf` is not part of Obsidian's public TypeScript API. This is a commonly used internal property but could change in future Obsidian versions.

View file

@ -0,0 +1,42 @@
# ADR-0002: Ribbon click behavior
**Date:** 2026-03-25
**Status:** Accepted
**Affects:** `RibbonService.ts`, `OpenTodayStreamCommand.ts`, `OpenTodayPrimaryStreamCommand.ts`
## Context
Users interact with stream ribbon icons to quickly open a stream's daily note. Before this change, every click behaved identically — there was no way to control *how* the note opened (reuse a tab vs. open a new one) at click time. Users who wanted to open the same stream in a second tab had no mechanism to do so from the ribbon.
Standard Obsidian and browser conventions use **Ctrl+click** (or **Cmd+click** on macOS) to force-open a link in a new tab.
## Decision
### Normal click (no modifier)
Follows the reuse-first strategy defined in [ADR-0001](0001-leaf-selection-behavior.md):
1. Reuse the active leaf if it's unpinned and in the main editor area
2. Otherwise find an existing unpinned leaf in the main editor area
3. Create a new tab only as a last resort
This respects the user's `reuseCurrentTab` setting and keeps tab count manageable.
### Ctrl+click / Cmd+click
Always opens in a **new tab**, bypassing all reuse logic:
1. The ribbon callback captures the `MouseEvent`
2. If `evt.ctrlKey` (Windows/Linux) or `evt.metaKey` (macOS) is `true`, a fresh leaf is created via `app.workspace.getLeaf('tab')`
3. This leaf is passed as `targetLeaf` to the command, which forwards it to `openStreamDate()`
4. When `targetLeaf` is provided, `LeafSelectionService` is not consulted at all
### Implementation
Both ribbon callbacks (`createAllStreamsIcon` and `createStreamIcons`) were updated from `() => { ... }` to `(evt: MouseEvent) => { ... }`. The `OpenTodayStreamCommand` constructor was extended with an optional `targetLeaf` parameter (matching `OpenTodayPrimaryStreamCommand`, which already had one).
## Consequences
- **Matches platform conventions.** Ctrl/Cmd+click for "open in new tab" is muscle memory for most users.
- **No new settings required.** The modifier key approach is discoverable and doesn't add UI complexity.
- **Commands (palette) are unaffected.** Only ribbon icon clicks receive the mouse event; command palette invocations continue to use the standard reuse-first strategy.

View file

@ -0,0 +1,40 @@
# ADR-0003: Stream bar date synchronization on external navigation
**Date:** 2026-03-25
**Status:** Accepted
**Affects:** `StreamsBarComponent.ts`
## Context
The stream bar displays the currently viewed date and provides calendar navigation within a stream. When a user opens a stream file via the plugin (ribbon icon, command palette, calendar click), the plugin explicitly calls `DateStateManager.setCurrentDate()` as part of the `openStreamDate()` flow, keeping the bar in sync.
However, users can also navigate to stream files through **external** means:
- Clicking a file in the file explorer
- Following an internal link from another note
- Using Obsidian's Quick Switcher
- Navigating browser history (back/forward)
In these cases, the plugin's `openStreamDate()` is never called. The `StreamsBarComponent`'s `file-open` handler detected the correct stream via `updateStreamContext()`, but **never extracted the date from the filename**. This left the stream bar showing a stale date from whatever was previously viewed.
## Decision
`updateStreamContext()` now always extracts the date from the opened file's basename when the file is identified as belonging to a stream. The date is parsed from the `YYYY-MM-DD` prefix pattern and pushed to `DateStateManager.setCurrentDate()`.
```typescript
// Extract date from filename (e.g., "2026-03-25" from "2026-03-25.md")
const match = file.basename.match(/^\d{4}-\d{2}-\d{2}/);
if (match) {
const [year, month, day] = match[0].split('-').map(n => parseInt(n, 10));
this.dateStateManager.setCurrentDate(new Date(year, month - 1, day));
}
```
This runs on every `file-open` event where the file matches a stream, regardless of how the navigation was initiated.
## Consequences
- **Stream bar stays in sync** regardless of how the user navigated to the file.
- **Calendar highlights the correct date**, enabling adjacent-day navigation from the right starting point.
- **No redundant updates**`DateStateManager` already deduplicates if the date hasn't changed, so plugin-initiated navigation (which also calls `setCurrentDate`) does not cause double renders.
- **Assumes `YYYY-MM-DD` prefix** in filenames. Files that belong to a stream folder but don't follow this naming convention will not update the date display.

View file

@ -1,6 +1,6 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { builtinModules } from "module";
import fs from "fs";
import path from "path";
@ -29,7 +29,7 @@ const context = await esbuild.context({
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
...builtinModules],
format: "cjs",
target: "es2018",
logLevel: "info",

View file

@ -45,12 +45,6 @@ export default class StreamsPlugin extends Plugin implements StreamsAPI {
await this.saveSettings();
}
// Migration: ensure barStyle exists
if (!this.settings.barStyle) {
this.settings.barStyle = 'default';
await this.saveSettings();
}
// Migration: ensure encryptThisStream, disabled, and merge 'folder' into 'dateFormat' for existing streams
let needsSave = false;
for (const stream of this.settings.streams) {

View file

@ -1,17 +1,16 @@
{
"id": "streams",
"name": "Streams",
"version": "1.3.0",
"version": "1.3.4",
"minAppVersion": "0.15.0",
"description": "Create and manage multiple Daily Note streams. Stream <-> Daily Notes <-> Backlink.",
"author": "Floyd",
"authorUrl": "http://floyd.pro",
"authorUrl": "https://floyd.pro",
"fundingUrl": {
"GitHub Sponsor": "https://github.com/sponsors/bfloydd",
"Buy Me a Coffee": "https://buymeacoffee.com/floydpro",
"Venmo": "https://venmo.com/floydpro?txn=pay",
"PayPal": "https://paypal.me/bfloydd"
},
"isDesktopOnly": false,
"providesApi": true
"isDesktopOnly": false
}

18
package-lock.json generated
View file

@ -1,6 +1,6 @@
{
"name": "streams",
"version": "1.0.0",
"version": "1.3.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
@ -13,7 +13,6 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
@ -2376,19 +2375,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/builtin-modules": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@ -6502,4 +6488,4 @@
}
}
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "streams",
"version": "1.0.0",
"version": "1.3.4",
"description": "Streams plugin for Obsidian",
"main": "main.js",
"scripts": {
@ -19,7 +19,6 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"jest": "^29.5.0",
"jest-environment-jsdom": "^29.5.0",
@ -28,4 +27,4 @@
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}
}

View file

@ -39,7 +39,6 @@ export interface DefaultSettingsConfiguration {
readonly reuseCurrentTab: boolean;
readonly debugLoggingEnabled: boolean;
readonly barStyle: 'default';
}
// Main configuration interface
@ -109,8 +108,7 @@ export class ConfigurationService extends BaseSliceService implements Configurat
showStreamsBarComponent: true,
reuseCurrentTab: false,
debugLoggingEnabled: false,
barStyle: 'default'
debugLoggingEnabled: false
} as const
};
@ -226,8 +224,7 @@ export class ConfigurationService extends BaseSliceService implements Configurat
showStreamsBarComponent: this.config.defaults.showStreamsBarComponent,
reuseCurrentTab: this.config.defaults.reuseCurrentTab,
debugLoggingEnabled: this.config.defaults.debugLoggingEnabled,
barStyle: this.config.defaults.barStyle
debugLoggingEnabled: this.config.defaults.debugLoggingEnabled
};
}
}

View file

@ -24,8 +24,7 @@ export const DEFAULT_SETTINGS = {
primaryStreamId: null,
showStreamsBarComponent: true,
reuseCurrentTab: false,
debugLoggingEnabled: false,
barStyle: 'default' as const
debugLoggingEnabled: false
} as const;

View file

@ -41,7 +41,6 @@ export interface StreamsSettings {
reuseCurrentTab: boolean;
debugLoggingEnabled: boolean; // Whether debug logging is enabled by default
barStyle: 'default' | 'modern'; // Style variant for the streams bar
}
// Specific error data types for better type safety

View file

@ -92,11 +92,6 @@ export class ComponentLifecycleManager {
component.updateReuseCurrentTab(settings.reuseCurrentTab);
}
// Refresh bar style for all existing components
if (component && typeof component.refreshBarStyle === 'function') {
component.refreshBarStyle();
}
// Update streams list for all existing components
if (component && typeof component.updateStreamsList === 'function' && settings.streams) {
component.updateStreamsList(settings.streams);

View file

@ -2,32 +2,20 @@ import { Stream } from '../../shared/types';
import { setIcon } from 'obsidian';
import { DateNavigationService } from './DateNavigationService';
/**
* Interface for plugin that provides settings
*/
interface PluginInterface {
settings: {
barStyle?: 'default' | 'modern';
};
}
/**
* Manages component state for StreamsBarComponent
* Extracted to follow Single Responsibility Principle
*/
export class ComponentStateManager {
private plugin: PluginInterface | null;
private streams: Stream[];
private selectedStream: Stream;
private dateNavigationService: DateNavigationService;
constructor(
plugin: PluginInterface | null,
streams: Stream[],
selectedStream: Stream,
dateNavigationService: DateNavigationService
) {
this.plugin = plugin;
this.streams = streams;
this.selectedStream = selectedStream;
this.dateNavigationService = dateNavigationService;
@ -75,24 +63,6 @@ export class ComponentStateManager {
}
}
/**
* Apply bar style to a component based on settings
*/
applyBarStyle(component: HTMLElement): void {
if (!this.plugin?.settings) {
return;
}
const barStyle = this.plugin.settings.barStyle;
// Remove existing style classes
component.removeClass('modern-style');
// Apply the appropriate style class
if (barStyle === 'modern') {
component.addClass('modern-style');
}
}
/**
* Format the today button text based on the current date

View file

@ -226,7 +226,7 @@ export class ComponentUIBuilder {
private setupNavigationControls(navControls: HTMLElement, collapsedView: HTMLElement): void {
const prevDayButton = navControls.createDiv('streams-bar-day-nav prev-day');
prevDayButton.setText('←');
setIcon(prevDayButton, 'chevron-left');
prevDayButton.setAttribute('aria-label', 'Previous day');
this.eventRegistry.register(prevDayButton, 'click', async (e: Event) => {
e.stopPropagation();
@ -242,7 +242,7 @@ export class ComponentUIBuilder {
});
const nextDayButton = navControls.createDiv('streams-bar-day-nav next-day');
nextDayButton.setText('→');
setIcon(nextDayButton, 'chevron-right');
nextDayButton.setAttribute('aria-label', 'Next day');
this.eventRegistry.register(nextDayButton, 'click', async (e: Event) => {
e.stopPropagation();

View file

@ -22,7 +22,6 @@ import { getStreamBaseFolder } from '../file-operations/streamUtils';
interface PluginInterface {
settings: {
barStyle?: 'default' | 'modern';
};
saveSettings(): void;
setActiveStream(streamId: string, force?: boolean, suppressEvent?: boolean): Promise<void>;
@ -100,12 +99,11 @@ export class StreamsBarComponent extends Component {
this.eventRegistry = new EventHandlerRegistry();
this.viewContainerService = new ViewContainerService();
this.eventSubscriptionManager = new ComponentEventSubscriptionManager(this.dateStateManager);
this.stateManager = new ComponentStateManager(plugin, streams, stream, this.dateNavigationService);
this.stateManager = new ComponentStateManager(streams, stream, this.dateNavigationService);
this.currentMonthView = new Date();
this.component = document.createElement('div');
this.component.addClass('streams-bar-component');
this.stateManager.applyBarStyle(this.component);
this.initializeDateState(leaf);
this.eventSubscriptionManager.subscribeToDateChanges((state) => {
@ -116,15 +114,9 @@ export class StreamsBarComponent extends Component {
this.handleSettingsChange(settings);
});
const contentContainer = this.viewContainerService.findContentContainer(leaf);
if (!contentContainer) {
centralizedLogger.error('Could not find content container');
return;
}
this.viewContainerService.removeExistingComponents(leaf, '.streams-bar-component');
if (!this.viewContainerService.attachComponent(this.component, leaf, contentContainer)) {
if (!this.viewContainerService.attachComponent(this.component, leaf)) {
return;
}
@ -189,6 +181,20 @@ export class StreamsBarComponent extends Component {
if (this.selectedStream?.id !== stream.id) {
this.updateActiveStream(stream);
}
// Always update the date from the filename so the bar shows the correct
// date even when navigating to a stream file from outside the plugin
if (file) {
const match = file.basename.match(/^\d{4}-\d{2}-\d{2}/);
if (match) {
const [year, month, day] = match[0].split('-').map(n => parseInt(n, 10));
const date = new Date(year, month - 1, day);
if (!isNaN(date.getTime())) {
this.dateStateManager.setCurrentDate(date);
}
}
}
this.component.show();
} else {
// If no stream found for file, hide the component
@ -533,8 +539,6 @@ export class StreamsBarComponent extends Component {
}
private handleSettingsChange(settings: StreamsSettings): void {
this.stateManager.applyBarStyle(this.component);
if (settings.streams) {
// Update streams list in selector
this.updateStreamsList(settings.streams);
@ -552,8 +556,5 @@ export class StreamsBarComponent extends Component {
}
}
public refreshBarStyle(): void {
this.stateManager.applyBarStyle(this.component);
}
}

View file

@ -14,57 +14,6 @@ interface ViewWithContentEl extends View {
* Extracted from StreamsBarComponent to follow Single Responsibility Principle
*/
export class ViewContainerService {
/**
* Find the content container for a given leaf based on its view type
* @param leaf The workspace leaf to find the container for
* @returns The content container element, or null if not found
*/
findContentContainer(leaf: WorkspaceLeaf): HTMLElement | null {
const viewType = leaf.view.getViewType();
let contentContainer: HTMLElement | null = null;
if (viewType === 'markdown') {
const markdownView = leaf.view as MarkdownView;
contentContainer = markdownView.contentEl;
} else if (viewType === CREATE_FILE_VIEW_TYPE ||
viewType === INSTALL_MELD_VIEW_TYPE ||
viewType === CREATE_FILE_VIEW_ENCRYPTED_TYPE) {
const view = leaf.view as unknown as ViewWithContentEl;
if (!view) {
centralizedLogger.error(`View is null for viewType: ${viewType}`);
return null;
}
contentContainer = view.contentEl;
} else if (viewType === 'empty') {
// For empty views, try to find the view-content element
const viewContent = leaf.view.containerEl.querySelector('.view-content');
if (viewContent) {
contentContainer = viewContent as HTMLElement;
} else {
centralizedLogger.error('Could not find view-content for empty view');
return null;
}
} else if (viewType === 'file-explorer') {
// For file explorer, add to the main content area
const mainContent = leaf.view.containerEl.querySelector('.nav-files-container') ||
leaf.view.containerEl.querySelector('.nav-files') ||
leaf.view.containerEl;
contentContainer = mainContent as HTMLElement;
} else {
const view = leaf.view as unknown as ViewWithContentEl;
if (!view) {
centralizedLogger.error('View is null');
return null;
}
contentContainer = view.contentEl;
}
return contentContainer;
}
/**
* Check if a leaf is in the main editor area (not a sidebar)
* @param leaf The workspace leaf to check
@ -86,18 +35,19 @@ export class ViewContainerService {
existingComponents.forEach(component => {
component.remove();
});
leafContainer.removeClass('streams-leaf-active');
}
/**
* Attach a component to the DOM in the appropriate location
* @param component The component element to attach
* @param leaf The workspace leaf to attach to
* @param contentContainer The content container element
* @returns True if attachment was successful, false otherwise
*/
attachComponent(component: HTMLElement, leaf: WorkspaceLeaf, contentContainer: HTMLElement): boolean {
// Add class to content container
contentContainer.addClass('streams-markdown-view-content');
attachComponent(component: HTMLElement, leaf: WorkspaceLeaf): boolean {
// Add state class to the root leaf container to reliably target layout CSS
leaf.view.containerEl.addClass('streams-leaf-active');
// Only add the calendar component if we're in the main editor area
if (!this.isMainEditorLeaf(leaf)) {
@ -116,16 +66,9 @@ export class ViewContainerService {
const viewHeader = leafContainer.querySelector('.view-header');
if (viewHeader) {
// On mobile/phone, append inside the view-header as the last child
// On desktop, insert after the view-header
if (Platform.isPhone) {
viewHeader.appendChild(component);
} else if (viewHeader.parentElement) {
// Insert after the view-header for desktop
viewHeader.parentElement.insertBefore(component, viewHeader.nextSibling);
} else {
viewHeader.appendChild(component);
}
// Append inside the view-header for all platforms (mobile, tablet, desktop)
// to ensure consistent placement across the app
viewHeader.appendChild(component);
} else {
// Fallback: attach to the leaf container itself
leafContainer.insertBefore(component, leafContainer.firstChild);

View file

@ -151,8 +151,7 @@ describe('ServiceCoordinator', () => {
primaryStreamId: null,
showStreamsBarComponent: true,
reuseCurrentTab: false,
debugLoggingEnabled: false,
barStyle: 'default'
debugLoggingEnabled: false
};
serviceCoordinator.onSettingsChanged(newSettings);

View file

@ -54,8 +54,7 @@ describe('Calendar Navigation Integration Tests', () => {
showStreamsBarComponent: true,
reuseCurrentTab: false,
debugLoggingEnabled: false,
barStyle: 'default' as const
debugLoggingEnabled: false
}
};

View file

@ -122,6 +122,7 @@ export class CreateFileView extends ItemView {
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-container');
this.containerEl.addClass('streams-create-file-leaf');
this.createFileViewContent(this.contentEl);
}
@ -142,6 +143,7 @@ export class CreateFileView extends ItemView {
if (this.contentEl) {
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-container');
this.containerEl.addClass('streams-create-file-leaf');
this.createFileViewContent(this.contentEl);
}
}
@ -163,6 +165,7 @@ export class CreateFileView extends ItemView {
// Prepare our content element
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-container');
this.containerEl.addClass('streams-create-file-leaf');
// Content element styling is handled by CSS class

View file

@ -106,6 +106,7 @@ export class CreateFileViewEncrypted extends ItemView {
if (this.contentEl) {
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-encrypted-container');
this.containerEl.addClass('streams-create-file-encrypted-leaf');
this.createFileViewEncryptedContent(this.contentEl);
}
}
@ -123,6 +124,7 @@ export class CreateFileViewEncrypted extends ItemView {
if (this.contentEl) {
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-encrypted-container');
this.containerEl.addClass('streams-create-file-encrypted-leaf');
this.createFileViewEncryptedContent(this.contentEl);
}
}
@ -145,6 +147,7 @@ export class CreateFileViewEncrypted extends ItemView {
// Prepare our content element
this.contentEl.empty();
this.contentEl.addClass('streams-create-file-encrypted-container');
this.containerEl.addClass('streams-create-file-encrypted-leaf');
// Content element styling is handled by CSS class

View file

@ -6,6 +6,59 @@ import { centralizedLogger } from '../../shared/CentralizedLogger';
* Extracted from streamUtils.ts to eliminate code duplication and follow SRP
*/
export class LeafSelectionService {
/**
* Checks whether a workspace leaf is pinned.
* Obsidian exposes this via the internal `pinned` property on the leaf.
*/
private static isLeafPinned(leaf: WorkspaceLeaf): boolean {
return (leaf as any).pinned === true;
}
/**
* Checks whether a leaf belongs to the main editor area (rootSplit),
* as opposed to left/right side panes.
*/
private static isInMainEditorArea(app: App, leaf: WorkspaceLeaf): boolean {
return leaf.getRoot() === app.workspace.rootSplit;
}
/**
* Finds the next unpinned leaf in the main editor area that can be reused.
* Mimics Obsidian's native behavior of selecting an existing tab rather
* than always creating a new one. Only considers leaves in the main
* editor area, never side panes.
* @param app - Obsidian app instance
* @param viewTypeFilter - Optional filter to check if a leaf's view type is suitable
* @returns An unpinned leaf in the main area, or a new tab if none found
*/
private static findNextUnpinnedLeaf(
app: App,
viewTypeFilter?: (viewType: string) => boolean
): WorkspaceLeaf | null {
// Scan only main editor leaves for an unpinned candidate
const candidates: WorkspaceLeaf[] = [];
app.workspace.iterateRootLeaves((leaf: WorkspaceLeaf) => {
if (!this.isInMainEditorArea(app, leaf)) return;
if (!this.isLeafPinned(leaf)) {
if (!viewTypeFilter || viewTypeFilter(leaf.view.getViewType())) {
candidates.push(leaf);
}
}
});
if (candidates.length > 0) {
return candidates[0];
}
// No unpinned leaf found in main area — create a new tab as last resort
try {
return app.workspace.getLeaf('tab');
} catch (error) {
centralizedLogger.error('Failed to create new leaf:', error);
return null;
}
}
/**
* Selects the appropriate leaf based on reuseCurrentTab setting
* @param app - Obsidian app instance
@ -30,16 +83,12 @@ export class LeafSelectionService {
*/
private static reuseCurrentLeaf(app: App): WorkspaceLeaf | null {
const activeLeaf = app.workspace.activeLeaf;
if (activeLeaf) {
if (activeLeaf && !this.isLeafPinned(activeLeaf) && this.isInMainEditorArea(app, activeLeaf)) {
return activeLeaf;
}
try {
return app.workspace.getLeaf('tab');
} catch (error) {
centralizedLogger.error('Failed to create new leaf:', error);
return null;
}
// Active leaf is pinned or in a side pane — find the next unpinned leaf instead
return this.findNextUnpinnedLeaf(app);
}
/**
@ -51,19 +100,15 @@ export class LeafSelectionService {
): WorkspaceLeaf | null {
const activeLeaf = app.workspace.activeLeaf;
if (activeLeaf) {
if (activeLeaf && !this.isLeafPinned(activeLeaf) && this.isInMainEditorArea(app, activeLeaf)) {
const viewType = activeLeaf.view.getViewType();
if (!viewTypeFilter || viewTypeFilter(viewType)) {
return activeLeaf;
}
}
try {
return app.workspace.getLeaf('tab');
} catch (error) {
centralizedLogger.error('Failed to create new leaf:', error);
return null;
}
// Active leaf is pinned, filtered out, or in a side pane — find the next unpinned leaf
return this.findNextUnpinnedLeaf(app, viewTypeFilter);
}
/**
@ -78,10 +123,11 @@ export class LeafSelectionService {
file: TFile,
reuseCurrentTab: boolean
): WorkspaceLeaf | null {
// First, try to find existing leaf with this file
// First, try to find existing leaf with this file (only in main editor area)
const existingLeaf = app.workspace.getLeavesOfType('markdown')
.find(leaf => {
try {
if (!this.isInMainEditorArea(app, leaf)) return false;
const view = leaf.view as MarkdownView;
const viewFile = view?.file;
if (!viewFile || !file) return false;
@ -101,18 +147,13 @@ export class LeafSelectionService {
// If not found and reuseCurrentTab is enabled, try to reuse current leaf
if (reuseCurrentTab) {
const activeLeaf = app.workspace.activeLeaf;
if (activeLeaf) {
if (activeLeaf && !this.isLeafPinned(activeLeaf) && this.isInMainEditorArea(app, activeLeaf)) {
return activeLeaf;
}
}
// Create a new leaf
try {
return app.workspace.getLeaf('tab');
} catch (error) {
centralizedLogger.error('Failed to create new leaf:', error);
return null;
}
// Find the next unpinned leaf in the main area, or create a new tab as last resort
return this.findNextUnpinnedLeaf(app);
}
}

View file

@ -1,4 +1,4 @@
import { App } from 'obsidian';
import { App, WorkspaceLeaf } from 'obsidian';
import { Stream } from '../../shared/types';
import { openStreamDate } from './streamUtils';
import { Logger } from '../debug-logging/Logger';
@ -11,12 +11,13 @@ export class OpenTodayStreamCommand implements Command {
constructor(
private app: App,
private stream: Stream,
private reuseCurrentTab: boolean = false
private reuseCurrentTab: boolean = false,
private targetLeaf?: WorkspaceLeaf
) {}
async execute(): Promise<void> {
log.debug(`Opening today's note for stream: ${this.stream.name}`);
log.debug(`Reuse current tab: ${this.reuseCurrentTab}`);
await openStreamDate(this.app, this.stream, new Date(), this.reuseCurrentTab);
await openStreamDate(this.app, this.stream, new Date(), this.reuseCurrentTab, this.targetLeaf);
}
}

View file

@ -90,12 +90,19 @@ export class RibbonService extends SettingsAwareSliceService {
this.getPlugin().addRibbonIcon(
'calendar',
'Streams: Go to primary stream',
() => {
(evt: MouseEvent) => {
// Ctrl/Cmd+click forces a new tab
const forceNewTab = evt.ctrlKey || evt.metaKey;
const targetLeaf = forceNewTab
? this.getPlugin().app.workspace.getLeaf('tab')
: undefined;
const command = new OpenTodayPrimaryStreamCommand(
this.getPlugin().app,
this.getStreams(),
this.getPlugin(),
this.getSettings().reuseCurrentTab
this.getSettings().reuseCurrentTab,
targetLeaf
);
command.execute();
}
@ -156,11 +163,18 @@ export class RibbonService extends SettingsAwareSliceService {
const iconEl = this.getPlugin().addRibbonIcon(
stream.icon,
`Open today for ${stream.name}`,
() => {
(evt: MouseEvent) => {
// Ctrl/Cmd+click forces a new tab
const forceNewTab = evt.ctrlKey || evt.metaKey;
const targetLeaf = forceNewTab
? this.getPlugin().app.workspace.getLeaf('tab')
: undefined;
const command = new OpenTodayStreamCommand(
this.getPlugin().app,
stream,
this.getSettings().reuseCurrentTab
this.getSettings().reuseCurrentTab,
targetLeaf
);
command.execute();
}

View file

@ -33,7 +33,6 @@ describe('RibbonService', () => {
mockSettings = {
streams: [],
primaryStreamId: null,
barStyle: 'default',
reuseCurrentTab: false,
// ... other settings as needed ...
} as any;

View file

@ -121,22 +121,6 @@ export class StreamsSettingTab extends PluginSettingTab {
new Notice(`Debug logging ${value ? 'enabled' : 'disabled'}`);
}));
new Setting(containerEl)
.setName('Bar style')
.setDesc('Choose the visual style for the streams bar component')
.addDropdown(dropdown => dropdown
.addOption('default', 'Default')
.addOption('modern', 'Modern')
.setValue(this.settingsManager.settings.barStyle)
.onChange(async (value: 'default' | 'modern') => {
this.settingsManager.settings.barStyle = value;
await this.settingsManager.saveSettings();
eventBus.emit(EVENTS.SETTINGS_CHANGED, this.settingsManager.settings, 'settings-management');
new Notice(`Bar style changed to ${value === 'default' ? 'Default' : 'Modern'}`);
}));
new Setting(containerEl).setName('Streams').setHeading();
new Setting(containerEl)

View file

@ -271,7 +271,7 @@
.streams-bar-component {
position: relative;
width: 100%;
background-color: var(--background-primary);
background-color: transparent;
padding: 4px 6px 4px 6px;
font-size: 14px;
line-height: 1.4;
@ -279,50 +279,118 @@
pointer-events: auto;
display: flex;
align-items: center;
border-bottom: 1px solid var(--background-modifier-border);
backdrop-filter: blur(8px);
border-bottom: none;
backdrop-filter: none;
z-index: 1000;
order: 99;
flex-basis: 100%;
}
.streams-markdown-view-content {
position: relative;
}
.streams-markdown-view-content .streams-bar-component {
position: absolute;
top: 10px;
}
.workspace-leaf-content:has(.streams-create-file-container) {
.streams-create-file-leaf {
width: 100%;
}
/* Prevent scrollbars on CreateFileView */
.workspace-leaf-content:has(.streams-create-file-container) .view-content {
.streams-create-file-leaf .view-content {
overflow: hidden;
}
.is-phone .view-header {
position: relative;
.streams-leaf-active .view-header {
z-index: 10000;
flex-wrap: wrap;
height: auto;
min-height: var(--header-height);
}
/* Ensure the native title container doesn't wrap to a new line, but shrinks and truncates as normal */
.streams-leaf-active .view-header>.view-header-title-container {
min-width: 0;
flex: 1 1 0;
}
/*
* HOLISTIC SCROLLING FIX (Desktop):
* Instead of making the header relative (which breaks immersive scrolling) or zeroing out padding,
* we let the header be native (absolute) so content scrolls under it.
* Since our Streams bar makes the header taller, we explicitly increase the padding-top
* on the markdown containers using an offset (40px).
*/
.mod-root .workspace-leaf-content.streams-leaf-active .view-content .markdown-reading-view>.markdown-preview-view,
.mod-root .workspace-leaf-content.streams-leaf-active .view-content .markdown-source-view>.cm-editor>.cm-scroller {
padding-top: calc(.5vh + var(--view-top-spacing-markdown, 0px)) !important;
}
/*
* HOLISTIC SCROLLING FIX (Mobile):
* Uses a slightly larger offset for touch targets (6vh).
*/
.is-phone .mod-root .workspace-leaf-content.streams-leaf-active .view-content .markdown-reading-view>.markdown-preview-view,
.is-phone .mod-root .workspace-leaf-content.streams-leaf-active .view-content .markdown-source-view>.cm-editor>.cm-scroller {
padding-top: calc(6vh + var(--view-top-spacing-markdown, 0px)) !important;
}
.is-phone .streams-bar-component {
position: absolute;
top: 45px;
backdrop-filter: none;
background-color: transparent;
border-bottom: none;
font-size: 11px;
z-index: 10001;
}
.is-phone .streams-bar-collapsed {
.streams-bar-component {
margin-top: -8px;
}
.streams-bar-collapsed {
background-color: transparent;
}
.is-phone .streams-bar-change-stream {
margin-right: 25px;
/*
* PILL STYLE MENU
* Wrap the two groups in pill-shaped containers to match Obsidian's native mobile menu buttons
*/
.streams-bar-nav-controls,
.streams-bar-change-stream {
background-color: var(--background-primary);
border-radius: var(--radius-xl, 20px);
padding: 4px 8px;
box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.05));
border: 1px solid var(--background-modifier-border);
height: 36px;
box-sizing: border-box;
}
/* Strip individual button borders/backgrounds to make them flow smoothly inside the pill */
.streams-bar-day-nav,
.streams-bar-today-button,
.streams-bar-home-button,
.streams-bar-settings-button {
background-color: transparent !important;
border: none !important;
box-shadow: none !important;
height: auto;
padding: 4px;
}
.streams-bar-day-nav:hover,
.streams-bar-today-button:hover,
.streams-bar-home-button:hover,
.streams-bar-settings-button:hover {
background-color: transparent !important;
color: var(--text-accent);
}
.streams-bar-change-stream:hover {
background-color: var(--background-secondary) !important;
color: var(--text-accent);
border-color: var(--background-modifier-border);
}
.streams-bar-nav-controls {
flex: 0 0 auto;
/* Prevent stretching */
}
.streams-bar-change-stream {
margin-right: 0;
/* Push all the way to the right */
}
.streams-bar-component .streams-dropdown {
@ -333,12 +401,14 @@
display: block;
}
.streams-bar-dropdown-hidden {
display: none !important;
body .streams-bar-dropdown-hidden,
.streams-bar-component .streams-bar-dropdown-hidden {
display: none;
}
.streams-bar-dropdown-visible {
display: block !important;
body .streams-bar-dropdown-visible,
.streams-bar-component .streams-bar-dropdown-visible {
display: block;
}
.streams-bar-component--visible {
@ -386,12 +456,15 @@
align-items: center;
justify-content: flex-start;
margin: 0;
background-color: transparent;
border-radius: 0;
padding: 0;
border: none;
gap: 8px;
flex: 1;
background-color: var(--background-primary);
border-radius: var(--radius-xl, 20px);
padding: 4px 8px;
box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.05));
border: 1px solid var(--background-modifier-border);
height: 36px;
box-sizing: border-box;
gap: 4px;
flex: 0 0 auto;
}
.streams-bar-nav {
@ -429,25 +502,25 @@
align-items: center;
justify-content: center;
width: 28px;
height: var(--streams-bar-widget-height);
height: auto;
border-radius: 4px;
cursor: pointer;
color: var(--text-muted);
transition: all 0.2s ease;
background-color: var(--interactive-normal);
border: 1px solid var(--background-modifier-border);
background-color: transparent;
border: none;
box-shadow: none;
font-size: 14px;
font-weight: 500;
}
.streams-bar-day-nav:hover {
background-color: var(--interactive-hover);
color: var(--text-normal);
border-color: var(--text-accent);
background-color: transparent;
color: var(--text-accent);
}
.streams-bar-day-nav:active {
background-color: var(--interactive-accent-hover);
background-color: transparent;
transform: scale(0.95);
}
@ -460,18 +533,18 @@
/* margin: 0 8px; */
font-size: 15px;
min-width: 60px;
height: var(--streams-bar-widget-height);
padding: 2px 2px;
height: auto;
padding: 4px;
border-radius: 4px;
background-color: var(--interactive-normal);
border: 1px solid var(--background-modifier-border);
background-color: transparent;
border: none;
box-shadow: none;
cursor: pointer;
/* transition: all 0.2s ease; */
}
.streams-bar-today-button:hover {
background-color: var(--interactive-hover);
border-color: var(--text-accent);
background-color: transparent;
color: var(--text-accent);
}
@ -492,23 +565,25 @@
align-items: center;
justify-content: center;
gap: 6px;
padding: 5px 12px;
height: var(--streams-bar-widget-height);
border-radius: 4px;
padding: 4px 16px;
height: 36px;
border-radius: var(--radius-xl, 20px);
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-muted);
margin: 0;
background-color: var(--interactive-normal);
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.05));
font-weight: 500;
position: relative;
box-sizing: border-box;
}
.streams-bar-change-stream:hover {
background-color: var(--interactive-hover);
color: var(--text-normal);
border-color: var(--text-accent);
background-color: var(--background-secondary);
color: var(--text-accent);
border-color: var(--background-modifier-border);
}
.streams-bar-change-stream-text {
@ -524,17 +599,18 @@
align-items: center;
justify-content: center;
width: 28px;
height: var(--streams-bar-widget-height);
height: auto;
padding: 4px;
border-radius: 4px;
background-color: var(--interactive-normal);
border: 1px solid var(--background-modifier-border);
background-color: transparent;
border: none;
box-shadow: none;
color: var(--text-muted);
}
.streams-bar-home-button:hover {
background-color: var(--interactive-hover);
color: var(--text-normal);
border-color: var(--text-accent);
background-color: transparent;
color: var(--text-accent);
}
.streams-bar-settings-button {
@ -544,17 +620,18 @@
align-items: center;
justify-content: center;
width: 28px;
height: var(--streams-bar-widget-height);
height: auto;
padding: 4px;
border-radius: 4px;
background-color: var(--interactive-normal);
border: 1px solid var(--background-modifier-border);
background-color: transparent;
border: none;
box-shadow: none;
color: var(--text-muted);
}
.streams-bar-settings-button:hover {
background-color: var(--interactive-hover);
color: var(--text-normal);
border-color: var(--text-accent);
background-color: transparent;
color: var(--text-accent);
}
/*********************************************************
@ -575,9 +652,9 @@
display: none;
}
.streams-bar-streams-dropdown.streams-bar-dropdown-visible,
.streams-bar-streams-dropdown.streams-dropdown--visible {
display: block !important;
.streams-bar-component .streams-bar-streams-dropdown.streams-bar-dropdown-visible,
.streams-bar-component .streams-bar-streams-dropdown.streams-dropdown--visible {
display: block;
}
.streams-bar-stream-item {
@ -1100,14 +1177,11 @@
.is-phone .streams-bar-component.modern-style {
position: absolute;
top: 45px;
backdrop-filter: none;
background: transparent;
border-bottom: none;
font-size: 11px;
padding: 6px 10px;
z-index: 10001;
}
.is-phone .streams-bar-component.modern-style .streams-bar-collapsed {
@ -1739,16 +1813,18 @@
position: relative;
}
.streams-empty-state-hidden {
display: none !important;
body .streams-empty-state-hidden,
div.streams-empty-state-hidden {
display: none;
visibility: hidden;
opacity: 0;
height: 0;
overflow: hidden;
}
.streams-date-setting-hidden {
display: none !important;
body .streams-date-setting-hidden,
div.streams-date-setting-hidden {
display: none;
}
.streams-install-meld-content {
@ -1919,12 +1995,12 @@
/*********************************************************
* ENCRYPTED CREATE FILE VIEW STYLES
*********************************************************/
.workspace-leaf-content:has(.streams-create-file-encrypted-container) {
.streams-create-file-encrypted-leaf {
width: 100%;
}
/* Prevent scrollbars on EncryptedCreateFileView */
.workspace-leaf-content:has(.streams-create-file-encrypted-container) .view-content {
.streams-create-file-encrypted-leaf .view-content {
overflow: hidden;
}

View file

@ -1,3 +1,3 @@
{
"1.0.0": "0.15.0"
}
}