mirror of
https://github.com/bfloydd/streams.git
synced 2026-07-22 12:50:25 +00:00
add all streams view
This commit is contained in:
parent
a9a79beb98
commit
9ba5e5be75
5 changed files with 585 additions and 1 deletions
14
README.md
14
README.md
|
|
@ -12,6 +12,7 @@
|
|||
- Full calendar for each stream.
|
||||
- New view for missing notes.
|
||||
- New view for viewing all notes in a stream.
|
||||
- "All Streams" dashboard view with statistics and quick actions.
|
||||
|
||||
## Getting started
|
||||
- Add new stream in settings, "Daily Notes", pointing to where you store your daily notes. There will be a ribbon button and a command to get to Today's Daily Note. Enable Full Stream view to get a view into your stream history. Disable core Daily Note plugin.
|
||||
|
|
@ -26,4 +27,15 @@
|
|||

|
||||
|
||||
## View Full Stream
|
||||

|
||||

|
||||
|
||||
## All Streams Dashboard
|
||||
The new All Streams view provides a comprehensive overview of all your configured streams. Access it via:
|
||||
- **Ribbon Button**: Click the dashboard icon in the left sidebar
|
||||
- **Command Palette**: Use "Open All Streams View"
|
||||
|
||||
This view displays each stream as a card showing:
|
||||
- Total number of files
|
||||
- Files created this year and month
|
||||
- Last modified date
|
||||
- Quick action buttons to open today's note or view the full stream
|
||||
34
main.ts
34
main.ts
|
|
@ -7,7 +7,9 @@ import { OpenTodayStreamCommand } from './src/commands/OpenTodayStreamCommand';
|
|||
import { StreamSelectionModal } from './src/modals/StreamSelectionModal';
|
||||
import { CREATE_FILE_VIEW_TYPE, CreateFileView } from './src/views/CreateFileView';
|
||||
import { STREAM_VIEW_TYPE, StreamView } from './src/views/StreamView';
|
||||
import { ALL_STREAMS_VIEW_TYPE, AllStreamsView } from './src/views/AllStreamsView';
|
||||
import { OpenStreamViewCommand } from './src/commands/OpenStreamViewCommand';
|
||||
import { OpenAllStreamsViewCommand } from './src/commands/OpenAllStreamsViewCommand';
|
||||
|
||||
const DEFAULT_SETTINGS: StreamsSettings = {
|
||||
streams: [],
|
||||
|
|
@ -36,6 +38,7 @@ export default class StreamsPlugin extends Plugin {
|
|||
this.initializeMobileIntegration();
|
||||
this.registerCalendarCommands();
|
||||
this.registerLogCommands();
|
||||
this.registerAllStreamsCommands();
|
||||
|
||||
this.addSettingTab(new StreamsSettingTab(this.app, this));
|
||||
this.initializeActiveView();
|
||||
|
|
@ -99,6 +102,12 @@ export default class StreamsPlugin extends Plugin {
|
|||
STREAM_VIEW_TYPE,
|
||||
(leaf) => this.createStreamViewFromState(leaf)
|
||||
);
|
||||
|
||||
// Register AllStreamsView
|
||||
this.registerView(
|
||||
ALL_STREAMS_VIEW_TYPE,
|
||||
(leaf) => new AllStreamsView(leaf, this.app)
|
||||
);
|
||||
}
|
||||
|
||||
private createStreamViewFromState(leaf: WorkspaceLeaf): StreamView {
|
||||
|
|
@ -131,6 +140,9 @@ export default class StreamsPlugin extends Plugin {
|
|||
}
|
||||
|
||||
private initializeAllRibbonIcons(): void {
|
||||
// Create the main All Streams ribbon icon
|
||||
this.createAllStreamsIcon();
|
||||
|
||||
// Create icons for all streams (even if hidden)
|
||||
this.settings.streams.forEach(stream => {
|
||||
this.createStreamIcons(stream);
|
||||
|
|
@ -140,6 +152,17 @@ export default class StreamsPlugin extends Plugin {
|
|||
this.updateAllIconVisibility();
|
||||
}
|
||||
|
||||
private createAllStreamsIcon(): void {
|
||||
this.addRibbonIcon(
|
||||
'layout-dashboard',
|
||||
'Streams: View All Streams',
|
||||
() => {
|
||||
const command = new OpenAllStreamsViewCommand(this.app);
|
||||
command.execute();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private createStreamIcons(stream: Stream): void {
|
||||
// Get or create entry for this stream
|
||||
let streamIcons = this.ribbonIconsByStream.get(stream.id);
|
||||
|
|
@ -743,6 +766,17 @@ export default class StreamsPlugin extends Plugin {
|
|||
});
|
||||
}
|
||||
|
||||
private registerAllStreamsCommands(): void {
|
||||
this.addCommand({
|
||||
id: 'open-all-streams-view',
|
||||
name: 'Open All Streams View',
|
||||
callback: () => {
|
||||
const command = new OpenAllStreamsViewCommand(this.app);
|
||||
command.execute();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private registerCalendarCommands(): void {
|
||||
this.addCommand({
|
||||
id: 'toggle-calendar-component',
|
||||
|
|
|
|||
33
src/commands/OpenAllStreamsViewCommand.ts
Normal file
33
src/commands/OpenAllStreamsViewCommand.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { App, WorkspaceLeaf } from 'obsidian';
|
||||
import { ALL_STREAMS_VIEW_TYPE, AllStreamsView } from '../views/AllStreamsView';
|
||||
|
||||
export class OpenAllStreamsViewCommand {
|
||||
private app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
async execute(): Promise<void> {
|
||||
// Look for existing All Streams view
|
||||
let leaf: WorkspaceLeaf | null = null;
|
||||
const existingLeaves = this.app.workspace.getLeavesOfType(ALL_STREAMS_VIEW_TYPE);
|
||||
|
||||
if (existingLeaves.length > 0) {
|
||||
// Reuse existing leaf
|
||||
leaf = existingLeaves[0];
|
||||
} else {
|
||||
// Create new leaf
|
||||
leaf = this.app.workspace.getLeaf('tab');
|
||||
}
|
||||
|
||||
// Set the view state
|
||||
await leaf.setViewState({
|
||||
type: ALL_STREAMS_VIEW_TYPE,
|
||||
state: {}
|
||||
});
|
||||
|
||||
// Activate the leaf
|
||||
this.app.workspace.setActiveLeaf(leaf, { focus: true });
|
||||
}
|
||||
}
|
||||
292
src/views/AllStreamsView.ts
Normal file
292
src/views/AllStreamsView.ts
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
import { App, ItemView, TFile, TFolder, WorkspaceLeaf } from 'obsidian';
|
||||
import { Stream } from '../../types';
|
||||
import { Logger } from '../utils/Logger';
|
||||
|
||||
export const ALL_STREAMS_VIEW_TYPE = 'streams-all-streams-view';
|
||||
|
||||
export class AllStreamsView extends ItemView {
|
||||
public app: App;
|
||||
private log: Logger;
|
||||
private streamsContainer: HTMLElement;
|
||||
private streams: Stream[] = [];
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, app: App) {
|
||||
super(leaf);
|
||||
this.app = app;
|
||||
this.log = new Logger();
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return ALL_STREAMS_VIEW_TYPE;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return 'All Streams';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'layout-dashboard';
|
||||
}
|
||||
|
||||
async onOpen(): Promise<void> {
|
||||
this.log.debug('Opening All Streams view');
|
||||
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass('streams-all-streams-container');
|
||||
|
||||
// Get streams from plugin settings
|
||||
const plugin = this.app.plugins.plugins['streams'];
|
||||
if (plugin) {
|
||||
this.streams = plugin.settings.streams;
|
||||
}
|
||||
|
||||
// Create header
|
||||
const header = container.createDiv('streams-all-streams-header');
|
||||
header.createEl('h1', { text: 'All Streams' });
|
||||
header.createEl('p', {
|
||||
text: `You have ${this.streams.length} stream${this.streams.length !== 1 ? 's' : ''} configured`,
|
||||
cls: 'streams-all-streams-subtitle'
|
||||
});
|
||||
|
||||
// Create streams container
|
||||
this.streamsContainer = container.createDiv('streams-all-streams-grid');
|
||||
|
||||
// Load and display streams
|
||||
await this.loadStreamsData();
|
||||
}
|
||||
|
||||
private async loadStreamsData(): Promise<void> {
|
||||
if (this.streams.length === 0) {
|
||||
this.showNoStreamsMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear existing content
|
||||
this.streamsContainer.empty();
|
||||
|
||||
// Create cards for each stream
|
||||
for (const stream of this.streams) {
|
||||
const card = await this.createStreamCard(stream);
|
||||
this.streamsContainer.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
private async createStreamCard(stream: Stream): Promise<HTMLElement> {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'streams-all-streams-card';
|
||||
|
||||
// Get stream statistics
|
||||
const stats = await this.getStreamStats(stream);
|
||||
|
||||
// Create card header
|
||||
const header = card.createDiv('streams-all-streams-card-header');
|
||||
const icon = header.createDiv('streams-all-streams-card-icon');
|
||||
// Use a simple icon representation
|
||||
icon.innerHTML = `<span class="streams-all-streams-card-icon-text">${stream.icon.charAt(0).toUpperCase()}</span>`;
|
||||
|
||||
const title = header.createEl('h3', { text: stream.name });
|
||||
title.className = 'streams-all-streams-card-title';
|
||||
|
||||
// Create card content
|
||||
const content = card.createDiv('streams-all-streams-card-content');
|
||||
|
||||
// Folder path
|
||||
const folderInfo = content.createDiv('streams-all-streams-card-folder');
|
||||
folderInfo.innerHTML = `<strong>Folder:</strong> <code>${stream.folder || 'Root'}</code>`;
|
||||
|
||||
// Statistics
|
||||
const statsContainer = content.createDiv('streams-all-streams-card-stats');
|
||||
|
||||
// Total files
|
||||
const totalFiles = statsContainer.createDiv('streams-all-streams-stat');
|
||||
totalFiles.innerHTML = `<span class="streams-all-streams-stat-label">Total Files:</span> <span class="streams-all-streams-stat-value">${stats.totalFiles}</span>`;
|
||||
|
||||
// This year files
|
||||
const thisYearFiles = statsContainer.createDiv('streams-all-streams-stat');
|
||||
thisYearFiles.innerHTML = `<span class="streams-all-streams-stat-label">This Year:</span> <span class="streams-all-streams-stat-value">${stats.thisYearFiles}</span>`;
|
||||
|
||||
// This month files
|
||||
const thisMonthFiles = statsContainer.createDiv('streams-all-streams-stat');
|
||||
thisMonthFiles.innerHTML = `<span class="streams-all-streams-stat-label">This Month:</span> <span class="streams-all-streams-stat-value">${stats.thisMonthFiles}</span>`;
|
||||
|
||||
// Last modified
|
||||
if (stats.lastModified) {
|
||||
const lastModified = statsContainer.createDiv('streams-all-streams-stat');
|
||||
lastModified.innerHTML = `<span class="streams-all-streams-stat-label">Last Modified:</span> <span class="streams-all-streams-stat-value">${stats.lastModified}</span>`;
|
||||
}
|
||||
|
||||
// Create card actions
|
||||
const actions = card.createDiv('streams-all-streams-card-actions');
|
||||
|
||||
// Today button
|
||||
const todayBtn = actions.createEl('button', { text: 'Today' });
|
||||
todayBtn.className = 'streams-all-streams-card-btn streams-all-streams-card-btn-primary';
|
||||
todayBtn.addEventListener('click', () => {
|
||||
this.openTodayStream(stream);
|
||||
});
|
||||
|
||||
// View All button
|
||||
const viewAllBtn = actions.createEl('button', { text: 'View All' });
|
||||
viewAllBtn.className = 'streams-all-streams-card-btn streams-all-streams-card-btn-secondary';
|
||||
viewAllBtn.addEventListener('click', () => {
|
||||
this.openStreamView(stream);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
private async getStreamStats(stream: Stream): Promise<{
|
||||
totalFiles: number;
|
||||
thisYearFiles: number;
|
||||
thisMonthFiles: number;
|
||||
lastModified: string | null;
|
||||
}> {
|
||||
try {
|
||||
if (!stream.folder) {
|
||||
return { totalFiles: 0, thisYearFiles: 0, thisMonthFiles: 0, lastModified: null };
|
||||
}
|
||||
|
||||
const folder = this.app.vault.getAbstractFileByPath(stream.folder);
|
||||
if (!(folder instanceof TFolder)) {
|
||||
return { totalFiles: 0, thisYearFiles: 0, thisMonthFiles: 0, lastModified: null };
|
||||
}
|
||||
|
||||
const files = this.getFilesInFolder(folder);
|
||||
const totalFiles = files.length;
|
||||
|
||||
const now = new Date();
|
||||
const thisYear = now.getFullYear();
|
||||
const thisMonth = now.getMonth();
|
||||
|
||||
let thisYearFiles = 0;
|
||||
let thisMonthFiles = 0;
|
||||
let lastModified: Date | null = null;
|
||||
|
||||
for (const file of files) {
|
||||
if (file instanceof TFile) {
|
||||
// Check if it's a date-based file (YYYY-MM-DD.md)
|
||||
const dateMatch = file.basename.match(/^\d{4}-\d{2}-\d{2}$/);
|
||||
if (dateMatch) {
|
||||
const fileDate = new Date(file.basename);
|
||||
if (fileDate.getFullYear() === thisYear) {
|
||||
thisYearFiles++;
|
||||
}
|
||||
if (fileDate.getFullYear() === thisYear && fileDate.getMonth() === thisMonth) {
|
||||
thisMonthFiles++;
|
||||
}
|
||||
}
|
||||
|
||||
// Track last modified
|
||||
if (!lastModified || file.stat.mtime > lastModified.getTime()) {
|
||||
lastModified = new Date(file.stat.mtime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalFiles,
|
||||
thisYearFiles,
|
||||
thisMonthFiles,
|
||||
lastModified: lastModified ? this.formatDate(lastModified) : null
|
||||
};
|
||||
} catch (error) {
|
||||
this.log.error('Error getting stream stats:', error);
|
||||
return { totalFiles: 0, thisYearFiles: 0, thisMonthFiles: 0, lastModified: null };
|
||||
}
|
||||
}
|
||||
|
||||
private getFilesInFolder(folder: TFolder): TFile[] {
|
||||
const files: TFile[] = [];
|
||||
|
||||
function recurseFolder(currentFolder: TFolder) {
|
||||
for (const child of currentFolder.children) {
|
||||
if (child instanceof TFile) {
|
||||
files.push(child);
|
||||
} else if (child instanceof TFolder) {
|
||||
recurseFolder(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recurseFolder(folder);
|
||||
return files;
|
||||
}
|
||||
|
||||
private formatDate(date: Date): string {
|
||||
const now = new Date();
|
||||
const diffTime = Math.abs(now.getTime() - date.getTime());
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffDays === 0) {
|
||||
return 'Today';
|
||||
} else if (diffDays === 1) {
|
||||
return 'Yesterday';
|
||||
} else if (diffDays < 7) {
|
||||
return `${diffDays} days ago`;
|
||||
} else if (diffDays < 30) {
|
||||
const weeks = Math.floor(diffDays / 7);
|
||||
return `${weeks} week${weeks !== 1 ? 's' : ''} ago`;
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
}
|
||||
|
||||
private showNoStreamsMessage(): void {
|
||||
this.streamsContainer.innerHTML = `
|
||||
<div class="streams-all-streams-empty">
|
||||
<div class="streams-all-streams-empty-icon">📝</div>
|
||||
<h3>No Streams Configured</h3>
|
||||
<p>You haven't configured any streams yet. Go to Settings → Streams to add your first stream.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private openTodayStream(stream: Stream): void {
|
||||
// Use the existing command to open today's stream
|
||||
try {
|
||||
this.app.commands.executeCommandById(`open-${stream.id}`);
|
||||
} catch (error) {
|
||||
this.log.error('Error executing today command:', error);
|
||||
// Fallback: try to open the stream directly
|
||||
this.openStreamDate(stream, new Date());
|
||||
}
|
||||
}
|
||||
|
||||
private openStreamView(stream: Stream): void {
|
||||
// Use the existing command to open the full stream view
|
||||
try {
|
||||
this.app.commands.executeCommandById(`view-${stream.id}`);
|
||||
} catch (error) {
|
||||
this.log.error('Error executing view command:', error);
|
||||
// Fallback: try to open the stream view directly
|
||||
this.openStreamViewDirect(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private async openStreamDate(stream: Stream, date: Date): Promise<void> {
|
||||
// Direct fallback for opening stream date
|
||||
const plugin = this.app.plugins.plugins['streams'];
|
||||
if (plugin) {
|
||||
// Import and use the utility function directly
|
||||
const { openStreamDate } = await import('../utils/streamUtils');
|
||||
openStreamDate(this.app, stream, date);
|
||||
}
|
||||
}
|
||||
|
||||
private openStreamViewDirect(stream: Stream): void {
|
||||
// Direct fallback for opening stream view
|
||||
const plugin = this.app.plugins.plugins['streams'];
|
||||
if (plugin) {
|
||||
// Import and use the command directly
|
||||
import('../commands/OpenStreamViewCommand').then(({ OpenStreamViewCommand }) => {
|
||||
const command = new OpenStreamViewCommand(this.app, stream);
|
||||
command.execute();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
this.log.debug('Closing All Streams view');
|
||||
}
|
||||
}
|
||||
213
styles.css
213
styles.css
|
|
@ -1,3 +1,216 @@
|
|||
/*********************************************************
|
||||
* ALL STREAMS VIEW STYLES
|
||||
*********************************************************/
|
||||
|
||||
.streams-all-streams-container {
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.streams-all-streams-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.streams-all-streams-header h1 {
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.streams-all-streams-subtitle {
|
||||
font-size: 1.2em;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.streams-all-streams-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card {
|
||||
background: var(--background-secondary);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.streams-all-streams-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--background-primary);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-accent);
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-icon-text {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-title {
|
||||
margin: 0;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-folder {
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: var(--background-primary);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-folder code {
|
||||
background: var(--background-secondary);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-stats {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.streams-all-streams-stat {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.streams-all-streams-stat-label {
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.streams-all-streams-stat-value {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-btn {
|
||||
flex: 1;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-btn-primary {
|
||||
background: var(--text-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-btn-primary:hover {
|
||||
background: var(--text-accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-btn-secondary {
|
||||
background: var(--background-primary);
|
||||
color: var(--text-normal);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.streams-all-streams-card-btn-secondary:hover {
|
||||
background: var(--background-secondary);
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.streams-all-streams-empty {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.streams-all-streams-empty-icon {
|
||||
font-size: 4em;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.streams-all-streams-empty h3 {
|
||||
margin-bottom: 16px;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.streams-all-streams-empty p {
|
||||
font-size: 1.1em;
|
||||
line-height: 1.6;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.streams-all-streams-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-stats {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.streams-all-streams-card-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/*********************************************************
|
||||
* LAYOUT & CONTAINER STYLES
|
||||
*********************************************************/
|
||||
|
|
|
|||
Loading…
Reference in a new issue