- Changed specification mechanism from "class" to "layout" for built-in menu styles

- Introduced three new layouts (default, minimal, slate) replacing class-based themes
- Added comprehensive CSS variables for full customization of colors, fonts, borders, and link types
- Updated README with new usage syntax, layout examples, and removed outdated todos
This commit is contained in:
2025-09-25 20:32:10 -04:00
parent 7697252ba3
commit 7c2a5367a3
4 changed files with 434 additions and 181 deletions

236
README.md
View File

@@ -1,27 +1,14 @@
# Menu Plugin for Obsidian # Menu Plugin for Obsidian
A flexible Obsidian plugin that allows you to create custom menus using code blocks with support for internal links, external links, and local file links. Create custom navigation menus in Obsidian using simple code blocks. Supports internal links, external URLs, and local file links with built-in themes and full customization.
## Basic Usage
## Todo Create a menu using a `menu` code block with one of the built-in layouts:
- [ ] Maybe change the format from using "class" to using "layout" and have them numbered? Like grid, flex, etc.
## Features
- 🔗 **Multiple Link Types**: Support for Obsidian internal links, external web links, and local file links
- 🎨 **Three Built-in Themes**: Default, minimal, and enhanced styling options
- ⚙️ **Custom Styling**: Full control over appearance with custom CSS classes
- 📁 **File System Integration**: Click to open local files and folders with system default applications
- 🔄 **Hot Reload Support**: Development-friendly with automatic reloading
## Usage
Create a menu using a `menu` code block:
````markdown ````markdown
```menu ```menu
class: default layout: default
[[Home]] [[Home]]
[[Projects|My Projects]] [[Projects|My Projects]]
[Google](https://google.com) [Google](https://google.com)
@@ -29,118 +16,149 @@ class: default
``` ```
```` ````
### Syntax ## Built-in Layouts
- **First line**: `class: {theme-name}` - Specifies the styling theme
- **Subsequent lines**: Links in various formats
### Link Types
#### Internal Links (Obsidian Notes)
```
[[Note Name]] # Links to "Note Name.md"
[[Note Name|Display]] # Links to "Note Name.md" but displays "Display"
```
#### External Links (Web URLs)
```
[Display Text](https://example.com)
```
#### File Links (Local Files/Folders)
```
[My Documents](file:///C:/Users/YourName/Documents)
[Project Folder](file:///C:/Users/YourName/Projects)
[PDF File](file:///C:/Users/YourName/document.pdf)
```
## Built-in Themes
### Default Theme
The standard theme with borders, backgrounds, and distinct styling for each link type.
### `default`
Standard buttons with borders and backgrounds
````markdown ````markdown
```menu ```menu
class: default layout: default
[[Home]] [[Dashboard]]
[Google](https://google.com) [GitHub](https://github.com)
[Documents](file:///C:/Users/YourName/Documents) [Files](file:///C:/Users/Documents)
``` ```
```` ````
### Minimal Theme ### `minimal`
Clean, text-only appearance with subtle color differentiation. Clean text links with subtle colors
````markdown ````markdown
```menu ```menu
class: minimal layout: minimal
[[Home]] [[Notes]]
[Google](https://google.com) [Web](https://example.com)
[Documents](file:///C:/Users/YourName/Documents) [Folder](file:///C:/Projects)
``` ```
```` ````
### `slate`
Solid background buttons
## Custom Styling
Create your own themes by adding CSS to your vault's snippets:
```css
.menu-container.my-custom-theme {
display: flex;
gap: 2em;
padding: 1em;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4);
border-radius: 10px;
}
.menu-container.my-custom-theme a {
color: white;
text-decoration: none;
padding: 0.5em 1em;
border-radius: 5px;
background: rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
}
.menu-container.my-custom-theme a:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
```
Then use it in your menu:
````markdown ````markdown
```menu ```menu
class: my-custom-theme layout: slate
[[Home]] [[Home]]
[Google](https://google.com) [Links](https://obsidian.md)
``` ```
```` ````
## Development ### `horizon`
Modern outlined style
### Setup ````markdown
```bash ```menu
npm install layout: horizon
npm run dev # Start development with auto-rebuild [[Dashboard]]
npm run build # Build for production [Resources](https://example.com)
``` ```
````
## Technical Details ### `aether`
Grid layout with equal-width items
````markdown
```menu
layout: aether
[[Projects]]
[GitHub](https://github.com)
[Documents](file:///C:/Users/Documents)
```
````
### Link Processing ## Link Types
- **Internal links**: Use Obsidian's `data-href` attribute for proper navigation
- **External links**: Open in new tab with security attributes
- **File links**: Use Electron's shell API to open with system default applications
### CSS Architecture - **Internal**: `[[Note Name]]` or `[[Note Name|Display Text]]`
- Base class: `.menu-container` - **External**: `[Display Text](https://example.com)`
- Theme classes: `.menu-container.{theme-name}` - **Files**: `[Display Text](file:///C:/path/to/file)`
- Link type classes: `.menu-internal-link`, `.menu-external-link`, `.menu-file-link`
## License ## Color Customization
MIT License - feel free to modify and distribute. Add color properties using YAML syntax:
````markdown
```menu
layout: default
bg: #1a1a1a
text: #ffffff
accent: #ff6b6b
border: #333333
[[Home]]
[GitHub](https://github.com)
```
````
### Global Color Variables
- `bg`: Background color
- `text`: Text color
- `accent`: Hover/accent color
- `border`: Border color
- `hover-bg`: Hover background
- `hover-border`: Hover border color
- `font`: Font family
### Link-Type Specific Colors
Customize each link type individually:
````markdown
```menu
layout: minimal
internal-text: #00ff00
external-text: #ff6600
file-text: #0066ff
internal-font: "Arial"
external-font: "Georgia"
[[Internal Link]]
[External Link](https://example.com)
[File Link](file:///C:/Documents)
```
````
**Available for each type** (`internal`, `external`, `file`):
- `{type}-text`: Text color
- `{type}-bg`: Background color
- `{type}-border`: Border color
- `{type}-font`: Font family
- `{type}-accent`: Hover text color
- `{type}-hover-bg`: Hover background
- `{type}-hover-border`: Hover border color
## Advanced Examples
### Gradient Background
````markdown
```menu
layout: horizon
bg: linear-gradient(45deg, #667eea, #764ba2)
text: white
accent: #ffd700
[[Dashboard]]
[Projects](https://github.com)
```
````
### Different Fonts Per Link Type
````markdown
```menu
layout: default
font: "Inter"
internal-font: "Fira Code"
external-font: "Georgia"
file-font: "Arial"
[[Code Notes]]
[Articles](https://medium.com)
[Local Files](file:///C:/Documents)
```
````
## Notes
- Use either `layout:` or `class:` (both work the same)
- File paths use `file://` protocol
- Colors support hex, rgb, hsl, CSS variables, and gradients
- Font names with spaces need quotes: `font: "Work Sans"`

28
main.js

File diff suppressed because one or more lines are too long

50
main.ts
View File

@@ -3,26 +3,60 @@ import { fileURLToPath } from 'url';
const { shell } = require('electron'); const { shell } = require('electron');
// Simple YAML parser for color properties
function parseYAML(text: string) {
const result: Record<string, string> = {};
const lines = text.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && trimmed.includes(':')) {
const [key, ...valueParts] = trimmed.split(':');
const value = valueParts.join(':').trim();
if (key && value) {
result[key.trim()] = value;
}
}
}
return result;
}
export default class MenuPlugin extends Plugin { export default class MenuPlugin extends Plugin {
async onload() { async onload() {
this.registerMarkdownCodeBlockProcessor('menu', (source, el, ctx) => { this.registerMarkdownCodeBlockProcessor('menu', (source, el, ctx) => {
// Parse the source
const lines = source.trim().split('\n'); const lines = source.trim().split('\n');
let cssClass = ''; let layout = '';
let colors: Record<string, string> = {};
const links: string[] = []; const links: string[] = [];
// Parse YAML-like properties and links
for (const line of lines) { for (const line of lines) {
const trimmed = line.trim(); const trimmed = line.trim();
if (trimmed.startsWith('class:')) { if (trimmed.startsWith('layout:') || trimmed.startsWith('class:')) {
cssClass = trimmed.substring(6).trim().replace(/[{}]/g, ''); const colonIndex = trimmed.indexOf(':');
} else if (trimmed) { layout = trimmed.substring(colonIndex + 1).trim();
} else if (trimmed.includes(':') && !trimmed.startsWith('[') && !trimmed.startsWith('[[')) {
// Parse color properties
const [key, ...valueParts] = trimmed.split(':');
const value = valueParts.join(':').trim();
if (key && value && !key.includes('//') && !key.includes('http')) {
colors[key.trim()] = value;
}
} else if (trimmed && !trimmed.includes(':')) {
links.push(trimmed);
} else if (trimmed.startsWith('[')) {
links.push(trimmed); links.push(trimmed);
} }
} }
// Create the container - if no class specified, use 'default' const finalLayout = layout || 'default';
const finalClass = cssClass || 'default'; const container = el.createEl('div', { cls: `menu-container ${finalLayout}` });
const container = el.createEl('div', { cls: `menu-container ${finalClass}` });
// Apply custom colors as CSS variables
if (Object.keys(colors).length > 0) {
for (const [key, value] of Object.entries(colors)) {
container.style.setProperty(`--${key}`, value);
}
}
// Process each link // Process each link
for (const link of links) { for (const link of links) {

View File

@@ -22,6 +22,17 @@
} }
} }
.menu-container,
.menu-container a {
transition: 125ms;
}
/*
•·················•·················•
| Menu Container Styles |
•·················•·················•
*/
/* Default style variant */ /* Default style variant */
.menu-container.default { .menu-container.default {
@@ -29,7 +40,8 @@
gap: 1em; gap: 1em;
flex-wrap: wrap; flex-wrap: wrap;
border-radius: 6px; border-radius: 6px;
background-color: var(--background-primary); background-color: var(--bg, var(--background-primary));
font-family: var(--font, inherit);
a { a {
padding: 0.6em 1em; padding: 0.6em 1em;
@@ -37,14 +49,59 @@
border-radius: 4px; border-radius: 4px;
transition: all 0.2s ease; transition: all 0.2s ease;
font-weight: 500; font-weight: 500;
border: 1px solid var(--background-modifier-border); border: 1px solid var(--border, var(--background-modifier-border));
color: var(--text-normal); color: var(--text, var(--text-normal));
font-family: var(--font, inherit);
&:hover { &:hover {
color: var(--text-accent); color: var(--accent, var(--text-accent));
/* background-color: var(--background-modifier-hover); */ border-color: var(--hover-border, var(--accent, var(--text-accent)));
border-color: var(--text-accent);
} }
&.menu-internal-link {
color: var(--internal-text, var(--text, var(--text-normal)));
border-color: var(--internal-border, var(--border, var(--background-modifier-border)));
background-color: var(--internal-bg, transparent);
font-family: var(--internal-font, var(--font, inherit));
&:hover {
color: var(--internal-accent, var(--accent, var(--text-accent)));
border-color: var(--internal-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--internal-hover-bg, var(--hover-bg, transparent));
}
}
&.menu-external-link {
color: var(--external-text, var(--text, var(--text-normal)));
border-color: var(--external-border, var(--border, var(--background-modifier-border)));
background-color: var(--external-bg, transparent);
font-family: var(--external-font, var(--font, inherit));
&:hover {
color: var(--external-accent, var(--accent, var(--text-accent)));
border-color: var(--external-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--external-hover-bg, var(--hover-bg, transparent));
}
}
&.menu-file-link {
color: var(--file-text, var(--text, var(--text-normal)));
border-color: var(--file-border, var(--border, var(--background-modifier-border)));
background-color: var(--file-bg, transparent);
font-family: var(--file-font, var(--font, inherit));
&:hover {
color: var(--file-accent, var(--accent, var(--text-accent)));
border-color: var(--file-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--file-hover-bg, var(--hover-bg, transparent));
}
}
}
&.wide a {
flex-grow: 1;
flex-basis: 150px;
text-align: center;
} }
} }
@@ -52,13 +109,7 @@
.menu-external-link .menu-external-link
.menu-internal-link */ .menu-internal-link */
.menu-container {
transition: 125ms;
a {
transition: 125ms;
}
}
/* Minimal style variant */ /* Minimal style variant */
.menu-container.minimal { .menu-container.minimal {
@@ -66,7 +117,8 @@
gap: 0.2em; gap: 0.2em;
flex-wrap: wrap; flex-wrap: wrap;
transition: 125ms; transition: 125ms;
background-color: var(--bg, transparent);
font-family: var(--font, inherit);
a { a {
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
@@ -75,34 +127,42 @@
transition: color 0.15s ease; transition: color 0.15s ease;
font-size: 0.9em; font-size: 0.9em;
font-weight: normal; font-weight: normal;
color: var(--text, var(--text-normal));
font-family: var(--font, inherit);
&:hover { &:hover {
background-color: var(--background-secondary); background-color: var(--hover-bg, var(--background-secondary));
color: var(--text-accent); color: var(--accent, var(--text-accent));
} }
}
a.menu-internal-link { &.menu-internal-link {
color: var(--text-accent); color: var(--internal-text, var(--internal, var(--text-accent)));
font-family: var(--internal-font, var(--font, inherit));
&:hover { &:hover {
color: var(--text-accent-hover); color: var(--internal-accent, var(--internal-hover, var(--text-accent-hover)));
background-color: var(--internal-hover-bg, var(--hover-bg, var(--background-secondary)));
}
} }
}
a.menu-external-link { &.menu-external-link {
color: var(--text-faint); color: var(--external-text, var(--external, var(--text-faint)));
font-family: var(--external-font, var(--font, inherit));
&:hover { &:hover {
color: var(--text-normal); color: var(--external-accent, var(--external-hover, var(--text-normal)));
background-color: var(--external-hover-bg, var(--hover-bg, var(--background-secondary)));
}
} }
}
a.menu-file-link { &.menu-file-link {
color: var(--text-faint); color: var(--file-text, var(--file, var(--text-faint)));
font-family: var(--file-font, var(--font, inherit));
&:hover { &:hover {
color: var(--text-normal); color: var(--file-accent, var(--file-hover, var(--text-normal)));
background-color: var(--file-hover-bg, var(--hover-bg, var(--background-secondary)));
}
} }
} }
} }
@@ -112,7 +172,8 @@
display: flex; display: flex;
gap: 0.2em; gap: 0.2em;
flex-wrap: wrap; flex-wrap: wrap;
background-color: var(--bg, transparent);
font-family: var(--font, inherit);
a { a {
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
@@ -121,18 +182,56 @@
transition: color 0.15s ease; transition: color 0.15s ease;
font-size: 0.9em; font-size: 0.9em;
font-weight: normal; font-weight: normal;
background-color: var(--background-secondary); background-color: var(--item-bg, var(--background-secondary));
border: 1px solid var(--background-secondary); border: 1px solid var(--border, var(--background-secondary));
color: var(--text-faint); color: var(--text, var(--text-faint));
font-family: var(--font, inherit);
&:hover { &:hover {
background-color: var(--background-secondary); background-color: var(--hover-bg, var(--background-secondary));
color: var(--text-accentl); color: var(--accent, var(--text-accent));
border: 1px solid var(--text-accent); border: 1px solid var(--hover-border, var(--accent, var(--text-accent)));
}
&.menu-internal-link {
color: var(--internal-text, var(--text, var(--text-faint)));
border-color: var(--internal-border, var(--border, var(--background-secondary)));
background-color: var(--internal-bg, var(--item-bg, var(--background-secondary)));
font-family: var(--internal-font, var(--font, inherit));
&:hover {
color: var(--internal-accent, var(--accent, var(--text-accent)));
border-color: var(--internal-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--internal-hover-bg, var(--hover-bg, var(--background-secondary)));
}
}
&.menu-external-link {
color: var(--external-text, var(--text, var(--text-faint)));
border-color: var(--external-border, var(--border, var(--background-secondary)));
background-color: var(--external-bg, var(--item-bg, var(--background-secondary)));
font-family: var(--external-font, var(--font, inherit));
&:hover {
color: var(--external-accent, var(--accent, var(--text-accent)));
border-color: var(--external-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--external-hover-bg, var(--hover-bg, var(--background-secondary)));
}
}
&.menu-file-link {
color: var(--file-text, var(--text, var(--text-faint)));
border-color: var(--file-border, var(--border, var(--background-secondary)));
background-color: var(--file-bg, var(--item-bg, var(--background-secondary)));
font-family: var(--file-font, var(--font, inherit));
&:hover {
color: var(--file-accent, var(--accent, var(--text-accent)));
border-color: var(--file-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--file-hover-bg, var(--hover-bg, var(--background-secondary)));
}
} }
} }
} }
@@ -141,38 +240,80 @@
gap: 0.5em; gap: 0.5em;
flex-wrap: wrap; flex-wrap: wrap;
border-radius: 8px; border-radius: 8px;
background-color: var(--background-primary); background-color: var(--bg, var(--background-primary));
font-family: var(--font, 'Space Grotesk', Inter, sans-serif);
a { a {
font-family: 'Space Grotesk', Inter, sans-serif; font-family: var(--font, 'Space Grotesk', Inter, sans-serif);
font-size: var(--fs, 1.1rem); font-size: var(--fs, 0.9rem);
white-space: nowrap; white-space: nowrap;
color: var(--cyan-500, darkcyan); color: var(--text, var(--cyan-500, darkcyan));
text-align: center; text-align: center;
text-decoration: none; text-decoration: none;
text-align: center; border: 1px solid var(--border, var(--cyan-500, darkcyan));
border: 1px solid var(--cyan-500, darkcyan);
border-radius: 3px; border-radius: 3px;
padding-block: var(--size-2, 0.5em); padding-block: var(--size-2, 0.5em);
padding-inline: var(--size-3, 2em); padding-inline: var(--size-3, 2em);
transition: 250ms; transition: 250ms;
&:hover { &:hover {
color: var(--text-accent); color: var(--accent, var(--orange-500, darkorange));
/* background-color: var(--background-modifier-hover); */ border: 1px solid var(--hover-border, var(--accent, var(--orange-500, darkorange)));
border: 1px solid var(--orange-500, darkorange);
color: var(--orange-500, darkorange);
} }
&.menu-internal-link {
color: var(--internal-text, var(--text, var(--cyan-500, darkcyan)));
border-color: var(--internal-border, var(--border, var(--cyan-500, darkcyan)));
background-color: var(--internal-bg, transparent);
font-family: var(--internal-font, var(--font, 'Space Grotesk', Inter, sans-serif));
&:hover {
color: var(--internal-accent, var(--accent, var(--orange-500, darkorange)));
border-color: var(--internal-hover-border, var(--hover-border, var(--accent, var(--orange-500, darkorange))));
background-color: var(--internal-hover-bg, var(--hover-bg, transparent));
}
}
&.menu-external-link {
color: var(--external-text, var(--text, var(--cyan-500, darkcyan)));
border-color: var(--external-border, var(--border, var(--cyan-500, darkcyan)));
background-color: var(--external-bg, transparent);
font-family: var(--external-font, var(--font, 'Space Grotesk', Inter, sans-serif));
&:hover {
color: var(--external-accent, var(--accent, var(--orange-500, darkorange)));
border-color: var(--external-hover-border, var(--hover-border, var(--accent, var(--orange-500, darkorange))));
background-color: var(--external-hover-bg, var(--hover-bg, transparent));
}
}
&.menu-file-link {
color: var(--file-text, var(--text, var(--cyan-500, darkcyan)));
border-color: var(--file-border, var(--border, var(--cyan-500, darkcyan)));
background-color: var(--file-bg, transparent);
font-family: var(--file-font, var(--font, 'Space Grotesk', Inter, sans-serif));
&:hover {
color: var(--file-accent, var(--accent, var(--orange-500, darkorange)));
border-color: var(--file-hover-border, var(--hover-border, var(--accent, var(--orange-500, darkorange))));
background-color: var(--file-hover-bg, var(--hover-bg, transparent));
}
}
}
&.wide a {
flex-grow: 1;
flex-basis: 150px;
text-align: center;
} }
} }
.menu-container.aether { .menu-container.aether {
display: grid; display: grid;
grid-template-columns: grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
repeat(auto-fit, minmax(150px, 1fr));
gap: 0.5em; gap: 0.5em;
background-color: var(--bg, transparent);
font-family: var(--font, inherit);
a { a {
text-align: center; text-align: center;
@@ -180,7 +321,53 @@
font-weight: 600; font-weight: 600;
padding: 0.5em 1em; padding: 0.5em 1em;
border-radius: 8px; border-radius: 8px;
color: var(--text, var(--text-normal));
border: 1px solid var(--border, var(--background-modifier-border));
font-family: var(--font, inherit);
border: 1px solid var(--background-modifier-border); &:hover {
color: var(--accent, var(--text-accent));
border-color: var(--hover-border, var(--accent, var(--text-accent)));
background-color: var(--hover-bg, var(--background-modifier-hover));
}
&.menu-internal-link {
color: var(--internal-text, var(--text, var(--text-normal)));
border-color: var(--internal-border, var(--border, var(--background-modifier-border)));
background-color: var(--internal-bg, transparent);
font-family: var(--internal-font, var(--font, inherit));
&:hover {
color: var(--internal-accent, var(--accent, var(--text-accent)));
border-color: var(--internal-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--internal-hover-bg, var(--hover-bg, var(--background-modifier-hover)));
}
}
&.menu-external-link {
color: var(--external-text, var(--text, var(--text-normal)));
border-color: var(--external-border, var(--border, var(--background-modifier-border)));
background-color: var(--external-bg, transparent);
font-family: var(--external-font, var(--font, inherit));
&:hover {
color: var(--external-accent, var(--accent, var(--text-accent)));
border-color: var(--external-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--external-hover-bg, var(--hover-bg, var(--background-modifier-hover)));
}
}
&.menu-file-link {
color: var(--file-text, var(--text, var(--text-normal)));
border-color: var(--file-border, var(--border, var(--background-modifier-border)));
background-color: var(--file-bg, transparent);
font-family: var(--file-font, var(--font, inherit));
&:hover {
color: var(--file-accent, var(--accent, var(--text-accent)));
border-color: var(--file-hover-border, var(--hover-border, var(--accent, var(--text-accent))));
background-color: var(--file-hover-bg, var(--hover-bg, var(--background-modifier-hover)));
}
}
} }
} }