Feat/plugin UI kit (#6362)

* fix(e2e): stabilize undo task delete sync test

Two flakiness sources fixed:
- Click on task element could activate title inline editor, causing
  Backspace to edit text instead of triggering delete. Now clicks the
  drag handle which calls focusSelf() without entering edit mode.
- Replaced deleteTask helper with inline sequence to avoid wasting
  2s of the 5s undo snackbar window on dialog-detection timeout.

* refactor: address code review findings from 2026-02-03

- Extract getBreakCycle helper to replace error-prone `cycle - 1 || 1`
  pattern at 3 call sites
- Add clarifying comment on intentionally broad 'timed out' match
- Reduce Pomodoro E2E test from 9 to 5 sessions (sufficient coverage)
- Remove dead _isTransientNetworkError wrapper from DropboxApi
- Extract stubWindowConfirm helper in task reducer tests

* fix(sync): prevent Formly from clearing provider config on show (#6345)

resetOnHide: true caused Formly to reset field values when provider
fieldGroups transitioned from hidden to visible, discarding user input
if sync was enabled before selecting a provider.

* fix(tasks): fix huge space between emoji and text in tag/project menus

Use matMenuItemIcon attribute on emoji spans so they project into the
icon slot of mat-menu-item instead of the text slot. Update emoji icon
sizing to 24x24px to match mat-icon and add overflow: hidden.

Closes #5977

* fix(tasks): guard against undefined task entities in selectors and archive (#6359)

Prevent TypeError crashes (reading 'dueWithTime', 'dueDay', 'issueProviderId') caused
by orphaned IDs in NgRx state. Fix archive merge to deduplicate IDs, filter orphans,
and use correct entity precedence (young over old). Add defensive null guards to
selectors and archive/task service methods.

* fix(tasks): guard against undefined task in mainListTasksInProject$ (#6360)

* fix(tasks): detect and sanitize orphaned task IDs to prevent startup crashes (#6359, #6360)

Orphaned task IDs (entries in task.ids without matching entities) caused
TypeError on app startup. Fix addresses three layers: validation now
flags orphaned IDs instead of silently skipping them, loadAllData
sanitizes IDs on load as a safety net, and data repair no longer crashes
when encountering orphaned IDs it's trying to fix.

* fix(sync): prevent recurring task duplication across clients

Remove SuperSync special-case that bypassed initial sync wait, causing
repeatable task effects to fire before sync completed. Add post-sync
cleanup effect that detects and removes stale duplicate repeat instances
when multiple active instances exist for the same repeat config.

* fix(sync): restore WebDAV provider compatibility warning text

* feat(sync): mark WebDAV and LocalFile sync options as experimental

* feat(plugins): add UI Kit with inject-first CSS strategy for iframe plugins

Introduce a lightweight CSS reset (UI Kit) that auto-styles basic HTML
elements in plugin iframes to match the host app theme. Injected after
<head> so plugin styles always win by source order.

UI Kit provides: element resets (body, headings, buttons, inputs, tables,
links, code, lists, hr), .btn-primary/.btn-outline button variants, and
.card/.card-clickable components.

All bundled plugins updated to use UI Kit classes, removing redundant
custom CSS (-542 lines net). Pico CSS removed from automations plugin.
sync-md converted from hardcoded colors to host theme variables.

* feat(plugins): extract shared CSS utilities into UI Kit

Move .text-muted, .text-primary, .page-fade and @keyframes fadeIn from
plugin CSS into the UI Kit so all iframe plugins get them automatically.
Add box-shadow focus ring to input:focus for better accessibility.
Remove per-plugin focus overrides now covered by the UI Kit.
This commit is contained in:
Johannes Millan 2026-02-04 18:18:22 +01:00 committed by GitHub
parent 2e4efb8b45
commit 4d22a64955
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 747 additions and 1120 deletions

View file

@ -1,5 +1,10 @@
# Plan: Plugin UI Consistency via CSS Library + Reactive Theme
## Status
- **Part 2 (CSS Component Library)**: Implemented as a UI Kit element reset in `src/app/plugins/util/plugin-ui-kit.css.ts`. Uses plain element selectors injected at the start of `<head>` (inject-first), so plugin CSS always wins by source order. No class-based components — just styled HTML elements.
- **Part 1 (Reactive Theme Updates / THEME_CHANGE hook)**: Not yet implemented.
## Goal
Make iframe plugin UI more consistent with the main app by:

View file

@ -85,21 +85,22 @@ The `manifest.json` file is required for all plugins and defines the plugin's me
### Manifest Fields
| Field | Type | Required | Description |
| ----------------- | -------- | -------- | ------------------------------------------------------------------ |
| `id` | string | ✓ | Unique identifier for your plugin (use kebab-case) |
| `name` | string | ✓ | Display name shown to users |
| `version` | string | ✓ | Semantic version (e.g., "1.0.0") |
| `description` | string | ✓ | Brief description of what your plugin does |
| `manifestVersion` | number | ✓ | Currently must be `1` |
| `minSupVersion` | string | ✓ | Minimum Super Productivity version required |
| `author` | string | | Plugin author name |
| `homepage` | string | | Plugin website or repository URL |
| `icon` | string | | Path to icon file (SVG recommended) |
| `iFrame` | boolean | | Whether plugin uses iframe UI (default: false) |
| `sidePanel` | boolean | | Show plugin in side panel (default: false), requires `iFrame:true` |
| `permissions` | string[] | | The permissions the plugin needs (e.g., ["nodeExecution"]) |
| `hooks` | string[] | | App events to listen to |
| Field | Type | Required | Description |
| ----------------- | -------- | -------- | -------------------------------------------------------------------------------------- |
| `id` | string | ✓ | Unique identifier for your plugin (use kebab-case) |
| `name` | string | ✓ | Display name shown to users |
| `version` | string | ✓ | Semantic version (e.g., "1.0.0") |
| `description` | string | ✓ | Brief description of what your plugin does |
| `manifestVersion` | number | ✓ | Currently must be `1` |
| `minSupVersion` | string | ✓ | Minimum Super Productivity version required |
| `author` | string | | Plugin author name |
| `homepage` | string | | Plugin website or repository URL |
| `icon` | string | | Path to icon file (SVG recommended) |
| `iFrame` | boolean | | Whether plugin uses iframe UI (default: false) |
| `sidePanel` | boolean | | Show plugin in side panel (default: false), requires `iFrame:true` |
| `permissions` | string[] | | The permissions the plugin needs (e.g., ["nodeExecution"]) |
| `hooks` | string[] | | App events to listen to |
| `uiKit` | boolean | | Enable UI Kit CSS reset for iframe plugins (default: true). Set to `false` to disable. |
### Complete Manifest Example
@ -172,37 +173,22 @@ Plugins that render custom UI in a sandboxed iframe.
<meta charset="UTF-8" />
<title>My Plugin UI</title>
<!-- CSS must be inlined -->
<!-- CSS must be inlined. Theme variables and UI Kit are injected automatically. -->
<style>
body {
font-family: Arial, sans-serif;
padding: 20px;
background: #f5f5f5;
padding: var(--s3);
}
.task-list {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: var(--card-bg);
border-radius: var(--card-border-radius);
padding: var(--s2);
box-shadow: var(--whiteframe-shadow-2dp);
}
.task-item {
padding: 8px;
border-bottom: 1px solid #eee;
}
button {
background: #4caf50;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #45a049;
padding: var(--s);
border-bottom: 1px solid var(--divider-color);
}
</style>
</head>
@ -249,6 +235,46 @@ Plugins that render custom UI in a sandboxed iframe.
</html>
```
### Theme Variables & UI Kit
Iframe plugins automatically receive:
1. **CSS variables** — All theme variables (colors, spacing, shadows, transitions) are injected as CSS custom properties on `:root`. Use `var(--c-primary)`, `var(--bg)`, `var(--text-color)`, etc.
2. **UI Kit CSS reset** — By default, basic HTML elements (`button`, `input`, `select`, `textarea`, `table`, `a`, `h1``h6`, `p`, `code`, `pre`, `hr`, etc.) are styled to match the app's look. This is injected before your plugin's own styles, so your CSS always wins.
To disable the UI Kit, add `"uiKit": false` to your manifest.
**Button variants:**
- Default `<button>` — Neutral card-background button with border
- `<button class="btn-primary">` — Filled primary-color button (white text)
- `<button class="btn-outline">` — Transparent button with primary-color border and text, fills on hover
**Card component:**
- `<div class="card">` — Card with background, shadow, rounded corners, and border
- `<div class="card card-clickable">` — Adds hover lift effect and primary border highlight
**Utility classes:**
- `.text-muted` — Muted text color (`var(--text-color-muted)`)
- `.text-primary` — Primary theme color (`var(--c-primary)`)
- `.page-fade` — Fade-in animation (0.3s ease)
**Key CSS variables:**
- `--bg`, `--bg-darker` — Background colors
- `--text-color`, `--text-color-muted` — Text colors
- `--c-primary`, `--c-accent`, `--c-warn` — Theme colors
- `--card-bg`, `--card-shadow`, `--card-border-radius` — Card styling
- `--divider-color` — Border/divider color
- `--s`, `--s2`, `--s3`, `--s4`, `--s-half`, `--s-quarter` — Spacing scale
- `--transition-standard` — Standard transition
- `--font-primary-stack` — App font stack
- `--whiteframe-shadow-1dp` through `--whiteframe-shadow-24dp` — Elevation shadows
- `--is-dark-theme``1` if dark theme, `0` if light
## Available API Methods
### Data Operations

View file

@ -17,11 +17,11 @@ The User Data Folder is the application data directory where the desktop app sto
## Default Location by Platform
| Platform | Location |
|----------|----------|
| **macOS** | `~/Library/Application Support/superProductivity/` |
| Platform | Location |
| ----------- | ----------------------------------------------------------------------------------------------- |
| **macOS** | `~/Library/Application Support/superProductivity/` |
| **Windows** | `C:\Users\<YOUR_USER_NAME>\AppData\Roaming\superProductivity/` or `%APPDATA%\superProductivity` |
| **Linux** | `~/.config/superProductivity/` |
| **Linux** | `~/.config/superProductivity/` |
The path is shown in the UI under Settings → Automatic Backups (without the backups subfolder) and is printed when the app is started from the command line.
@ -94,12 +94,12 @@ UI and app state stored with keys such as:
**Automatic backups:** When enabled in settings, the app creates a backup on a schedule (default: every 5 minutes). Storage location depends on platform:
| Platform | Where automatic backups are stored |
|----------|-----------------------------------|
| **Electron (desktop)** | `{userData}/backups/` as one JSON file per calendar day (`YYYY-MM-DD.json`) |
| **Android** | IndexedDB with key `backup` (single entry, overwritten) |
| **iOS** | Capacitor Filesystem API: `super-productivity-backup.json` in `Directory.Data` |
| **Web** | No automatic file backups; use "Export data" to download a file manually |
| Platform | Where automatic backups are stored |
| ---------------------- | ------------------------------------------------------------------------------ |
| **Electron (desktop)** | `{userData}/backups/` as one JSON file per calendar day (`YYYY-MM-DD.json`) |
| **Android** | IndexedDB with key `backup` (single entry, overwritten) |
| **iOS** | Capacitor Filesystem API: `super-productivity-backup.json` in `Directory.Data` |
| **Web** | No automatic file backups; use "Export data" to download a file manually |
**Manual backups:** (1) **Create manual backup** in Settings → Sync & Export creates a backup using the same mechanism as automatic backups (platform-dependent location). (2) **Safety backups** are created automatically before certain sync operations; the app keeps a limited number of slots (e.g. two most recent, one first from today, one first from day before). (3) **Export data** downloads a complete backup JSON file to a path you choose (or the browser download folder on web).

View file

@ -65,6 +65,7 @@ PluginAPI.registerShortcut({
"hooks": ["taskComplete", "taskUpdate"],
"permissions": ["showSnack", "getTasks", "addTask", "showIndexHtmlAsView"],
"iFrame": true,
"uiKit": true,
"icon": "icon.svg"
}
```

View file

@ -115,6 +115,7 @@ export interface PluginManifest {
icon?: string; // Path to SVG icon file relative to plugin root
nodeScriptConfig?: PluginNodeScriptConfig;
sidePanel?: boolean; // If true, plugin loads in right panel instead of route
uiKit?: boolean; // If false, skip injecting the UI kit CSS reset. Defaults to true.
jsonSchemaCfg?: string; // Path to JSON schema file for plugin configuration relative to plugin root
i18n?: {
languages: string[]; // Array of supported language codes (e.g., ['en', 'de', 'fr'])

View file

@ -1,120 +1,25 @@
/*
* AI Productivity Prompts Plugin Styles
* Simplified structure following KISS principles
* UI Kit provides base element styles (body, headings, buttons, inputs, etc.)
* Only plugin-specific layout and component rules here.
*/
* {
box-sizing: border-box;
}
/* Theme support with Super Productivity variables */
:root {
/* Light theme defaults */
--plugin-text-primary: var(--text-color, #333);
--plugin-text-muted: var(--text-color-muted, #666);
--plugin-bg-card: var(--card-bg, #fff);
--plugin-bg-input: var(--input-bg, #fff);
--plugin-border: var(--extra-border-color, #ddd);
--plugin-border-focus: var(--c-primary, #007bff);
--plugin-primary: var(--c-primary, #007bff);
--plugin-accent: var(--c-accent, #ff6b35);
--plugin-shadow: rgba(0, 0, 0, 0.1);
--plugin-shadow-hover: rgba(0, 0, 0, 0.15);
}
/* Dark theme support - using body class detection */
body.isEnabledDarkTheme {
--plugin-text-primary: #e0e0e0;
--plugin-text-muted: #aaa;
--plugin-bg-card: #2d2d2d;
--plugin-bg-input: #3d3d3d;
--plugin-border: #555;
--plugin-border-focus: #4a9eff;
--plugin-shadow: rgba(0, 0, 0, 0.4);
--plugin-shadow-hover: rgba(0, 0, 0, 0.6);
}
/* Fallback dark theme via prefers-color-scheme */
@media (prefers-color-scheme: dark) {
:root {
--plugin-text-primary: var(--text-color, #e0e0e0);
--plugin-text-muted: var(--text-color-muted, #aaa);
--plugin-bg-card: var(--card-bg, #2d2d2d);
--plugin-bg-input: var(--input-bg, #3d3d3d);
--plugin-border: var(--extra-border-color, #555);
--plugin-shadow: rgba(0, 0, 0, 0.4);
--plugin-shadow-hover: rgba(0, 0, 0, 0.6);
}
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Open Sans', sans-serif;
background: transparent;
color: var(--plugin-text-primary);
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 1rem;
}
/* Common card styles */
.card {
background: var(--plugin-bg-card);
border-radius: 8px;
box-shadow: 0 2px 4px var(--plugin-shadow);
padding: 1rem;
text-align: left;
border: 2px solid var(--plugin-border);
}
.card-clickable:hover {
transition: transform 0.2s;
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--plugin-shadow-hover);
border-color: var(--plugin-primary);
cursor: pointer;
}
/* Common text styles */
.text-muted {
color: var(--plugin-text-muted);
}
.text-primary {
color: var(--plugin-primary);
padding: var(--s2);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
margin-bottom: var(--s2);
}
.header h1 {
margin: 0;
font-size: 1.5rem;
color: var(--c-primary);
}
.back-button {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--c-primary);
color: var(--c-primary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.back-button:hover {
background: var(--c-primary);
color: var(--c-contrast);
}
.main {
@ -125,44 +30,33 @@ body {
.intro {
text-align: center;
margin-bottom: 2rem;
}
.intro h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
margin-bottom: var(--s3);
}
/* Category Grid */
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.category-card h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
gap: var(--s2);
margin-top: var(--s3);
}
.category-card p {
margin: 0;
font-size: 0.9rem;
}
/* Custom prompts card styling */
.custom-prompts-card {
border-left: 4px solid var(--plugin-accent);
border-left: 4px solid var(--c-accent);
background: linear-gradient(
135deg,
color-mix(in srgb, var(--plugin-accent) 10%, transparent),
color-mix(in srgb, var(--plugin-accent) 5%, transparent)
color-mix(in srgb, var(--c-accent) 10%, transparent),
color-mix(in srgb, var(--c-accent) 5%, transparent)
);
}
.custom-prompts-card:hover {
border-color: var(--plugin-accent);
border-color: var(--c-accent);
transform: translateY(-3px);
}
@ -173,10 +67,10 @@ body {
}
.selected-category {
padding: 1rem 0;
padding: var(--s2) 0;
text-align: center;
border-bottom: 1px solid var(--plugin-border);
margin-bottom: 2rem;
border-bottom: 1px solid var(--divider-color);
margin-bottom: var(--s3);
}
.selected-category h2 {
@ -187,26 +81,23 @@ body {
.prompt-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: var(--s);
}
.prompt-item {
padding: 1rem;
padding: var(--s2);
cursor: pointer;
transition: all 0.2s;
}
.prompt-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px var(--plugin-shadow-hover);
border-color: var(--plugin-primary);
box-shadow: var(--whiteframe-shadow-4dp);
border-color: var(--c-primary);
}
.prompt-title {
margin: 0;
font-size: 1rem;
font-weight: 600;
color: var(--plugin-text-primary);
}
/* Prompt View */
@ -216,12 +107,12 @@ body {
}
.selected-prompt-header {
padding: 1rem 0;
padding: var(--s2) 0;
text-align: center;
}
.selected-prompt-header h2 {
margin: 0 0 0.5rem 0;
margin: 0 0 var(--s) 0;
}
.selected-prompt-header p {
@ -229,15 +120,19 @@ body {
}
.generated-prompt {
padding: 1.5rem;
padding: var(--s3);
}
.generated-prompt h3 {
color: var(--c-primary);
}
.prompt-options {
margin-bottom: 1.5rem;
padding: 1rem;
background: color-mix(in srgb, var(--plugin-text-primary) 5%, transparent);
border-radius: 4px;
border: 1px solid var(--plugin-border);
margin-bottom: var(--s3);
padding: var(--s2);
background: var(--bg-darker);
border-radius: var(--card-border-radius);
border: 1px solid var(--divider-color);
width: 100%;
box-sizing: border-box;
overflow: hidden;
@ -246,9 +141,8 @@ body {
.dropdown-label {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9rem;
color: var(--plugin-text-primary);
gap: var(--s);
font-size: 0.9em;
flex-wrap: wrap;
width: 100%;
}
@ -256,42 +150,28 @@ body {
.task-dropdown {
width: 100%;
max-width: 100%;
padding: 0.5rem;
border: 1px solid var(--plugin-border);
border-radius: 4px;
background: var(--plugin-bg-card);
color: var(--plugin-text-primary);
font-size: 0.9rem;
font-family: inherit;
cursor: pointer;
box-sizing: border-box;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
.task-dropdown:focus {
outline: none;
border-color: var(--plugin-border-focus);
}
.task-preview {
margin-top: 0.75rem;
margin-top: var(--s);
}
.task-count-info {
font-size: 0.85rem;
color: var(--plugin-primary);
font-size: 0.85em;
color: var(--c-primary);
font-weight: 500;
}
.task-count-info.tooltip {
cursor: help;
border-bottom: 1px dotted var(--plugin-primary);
border-bottom: 1px dotted var(--c-primary);
}
.task-count-info.tooltip:hover {
color: var(--plugin-accent);
color: var(--c-accent);
}
.task-preview-content {
@ -299,148 +179,97 @@ body {
}
.task-count-header {
font-size: 0.85rem;
color: var(--plugin-primary);
font-size: 0.85em;
color: var(--c-primary);
font-weight: 600;
margin-bottom: 0.5rem;
margin-bottom: var(--s);
}
.task-preview-list {
margin-left: 0.5rem;
margin-left: var(--s);
}
.task-preview-item {
font-size: 0.8rem;
color: var(--plugin-text-primary);
margin-bottom: 0.25rem;
font-size: 0.8em;
margin-bottom: var(--s-quarter);
line-height: 1.3;
word-break: break-word;
}
.task-preview-more {
font-size: 0.75rem;
color: var(--plugin-text-muted);
font-size: 0.75em;
color: var(--text-color-muted);
font-style: italic;
margin-top: 0.25rem;
}
.generated-prompt h3 {
margin: 0 0 1rem 0;
color: var(--plugin-primary);
margin-top: var(--s-quarter);
}
.prompt-text {
width: 100%;
min-height: 300px;
padding: 1rem;
border: 1px solid var(--plugin-border);
border-radius: 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
font-size: 0.9rem;
font-size: 0.9em;
line-height: 1.5;
background: var(--plugin-bg-input);
color: var(--plugin-text-primary);
resize: vertical;
}
.prompt-actions {
margin-top: 1rem;
margin-top: var(--s2);
display: flex;
gap: 0.75rem;
gap: var(--s);
justify-content: center;
flex-wrap: wrap;
}
.action-button {
padding: 0.75rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.85rem;
font-family: inherit;
transition: all 0.2s;
font-weight: 600;
min-width: 80px;
text-decoration: none;
display: inline-block;
text-align: center;
}
.action-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px var(--plugin-shadow-hover);
}
.action-button:disabled {
opacity: 0.7;
transform: none;
cursor: not-allowed;
}
/* Copy button */
.copy-button {
background: var(--plugin-primary);
color: white;
}
/* ChatGPT button */
/* ChatGPT button — distinct brand color */
.chatgpt-button {
background: #10a37f;
color: white;
border-color: #10a37f;
}
.chatgpt-button:hover {
background: #0d8c6b;
border-color: #0d8c6b;
}
/* Custom prompt specific styles */
.empty-state {
text-align: center;
padding: 2rem;
background: var(--plugin-bg-card);
border-radius: 8px;
border: 2px dashed var(--plugin-border);
padding: var(--s3);
background: var(--card-bg);
border-radius: var(--card-border-radius);
border: 2px dashed var(--divider-color);
}
.loading-container {
text-align: center;
padding: 2rem;
padding: var(--s3);
}
.custom-prompts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
margin-bottom: var(--s3);
flex-wrap: wrap;
gap: 1rem;
gap: var(--s2);
}
.custom-prompts-list {
display: flex;
flex-direction: column;
gap: 1rem;
gap: var(--s2);
}
.custom-prompt-card {
border-left: 4px solid var(--plugin-primary);
border-left: 4px solid var(--c-primary);
}
.custom-prompt-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.prompt-actions {
display: flex;
gap: 0.5rem;
}
.prompt-preview {
margin-bottom: 1rem;
line-height: 1.5;
margin-bottom: var(--s2);
}
.prompt-card-actions {
@ -448,39 +277,14 @@ body {
justify-content: flex-end;
}
/* Action buttons */
.action-button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
font-weight: 500;
}
.action-button.primary {
background: var(--plugin-primary);
color: white;
}
.action-button.primary:hover {
filter: brightness(1.1);
}
.action-button.secondary {
background: color-mix(in srgb, var(--plugin-text-primary) 10%, transparent);
color: var(--plugin-text-primary);
border: 1px solid var(--plugin-border);
}
.action-button.secondary:hover {
background: color-mix(in srgb, var(--plugin-text-primary) 15%, transparent);
.prompt-preview {
margin-bottom: var(--s2);
line-height: 1.5;
}
/* Editor styles */
.editor-header {
margin-bottom: 1.5rem;
margin-bottom: var(--s3);
}
.editor-container {
@ -488,16 +292,15 @@ body {
}
.editor-instructions {
background: var(--plugin-bg-card);
padding: 1rem;
border-radius: 6px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--plugin-primary);
background: var(--card-bg);
padding: var(--s2);
border-radius: var(--card-border-radius);
margin-bottom: var(--s3);
border-left: 4px solid var(--c-primary);
}
.editor-instructions p {
margin: 0 0 0.5rem 0;
color: var(--plugin-text-primary);
margin: 0 0 var(--s) 0;
}
.editor-instructions p:last-child {
@ -505,81 +308,35 @@ body {
}
.editor-field {
margin-bottom: 1.5rem;
margin-bottom: var(--s3);
}
.field-label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
color: var(--plugin-text-primary);
margin-bottom: var(--s);
}
.prompt-title-input {
width: 100%;
padding: 0.75rem 1rem;
border: 2px solid var(--plugin-border);
border-radius: 6px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Open Sans', sans-serif;
font-size: 1rem;
font-weight: 500;
background: var(--plugin-bg-input);
color: var(--plugin-text-primary);
}
.prompt-title-input:focus {
outline: none;
border-color: var(--plugin-border-focus);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--plugin-border-focus) 20%, transparent);
}
.prompt-title-input::placeholder {
color: var(--plugin-text-muted);
font-weight: normal;
border-width: 2px;
}
.prompt-textarea {
width: 100%;
min-height: 300px;
padding: 1rem;
border: 2px solid var(--plugin-border);
border-radius: 6px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace;
font-size: 0.9rem;
font-size: 0.9em;
line-height: 1.5;
background: var(--plugin-bg-input);
color: var(--plugin-text-primary);
resize: vertical;
}
.prompt-textarea:focus {
outline: none;
border-color: var(--plugin-border-focus);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--plugin-border-focus) 20%, transparent);
}
.prompt-textarea::placeholder {
color: var(--plugin-text-muted);
border-width: 2px;
}
.editor-actions {
display: flex;
gap: 1rem;
gap: var(--s2);
justify-content: flex-end;
padding-top: 1rem;
border-top: 1px solid var(--plugin-border);
}
/* Simple page transition */
.page-fade {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
padding-top: var(--s2);
border-top: 1px solid var(--divider-color);
}

View file

@ -78,7 +78,7 @@ export const ProcrastinationInfo: Component<ProcrastinationInfoProps> = (props)
<div class="action-buttons">
<button
class="primary-button"
class="btn-primary"
onClick={props.onBackToWork}
>
Back to work!

View file

@ -130,14 +130,14 @@ Give me a specific action plan with time estimates."
<div class="editor-actions">
<button
class="action-button secondary"
class="btn-outline"
onClick={props.onCancel}
disabled={isSaving()}
>
Cancel
</button>
<button
class="action-button primary"
class="btn-primary"
onClick={handleSave}
disabled={isSaving() || !promptTitle().trim() || !promptText().trim()}
>

View file

@ -163,7 +163,7 @@ const PromptView: Component<PromptViewProps> = (props) => {
<div class="prompt-actions">
<Button onClick={handleCopyToClipboard}>{copyButtonText()}</Button>
<a
class="action-button chatgpt-button"
class="chatgpt-button"
href={createChatGPTUrl(generatedPrompt())}
target="_blank"
rel="noopener noreferrer"

View file

@ -3,7 +3,6 @@ import { Component, JSX } from 'solid-js';
interface ButtonProps {
onClick?: () => void;
variant?: 'primary' | 'secondary' | 'back';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
title?: string;
class?: string;
@ -12,18 +11,15 @@ interface ButtonProps {
export const Button: Component<ButtonProps> = (props) => {
const getButtonClass = () => {
const baseClass = 'button';
const variantClass = props.variant
? `${baseClass}-${props.variant}`
: 'action-button primary';
const sizeClass = props.size ? `${baseClass}-${props.size}` : '';
const customClass = props.class || '';
if (props.variant === 'back') {
return 'back-button';
switch (props.variant) {
case 'back':
case 'secondary':
return `btn-outline ${customClass}`.trim();
case 'primary':
default:
return `btn-primary ${customClass}`.trim();
}
return `${variantClass} ${sizeClass} ${customClass}`.trim();
};
return (

View file

@ -8,214 +8,230 @@
/>
<title>API Test Plugin Dashboard</title>
<style>
/* Theme vars + UI Kit are injected automatically.
Only layout rules that the UI Kit can't provide. */
body {
font-family: monospace;
margin: 0;
padding: 20px;
/* Use injected CSS variables for theming */
background: var(--bg, transparent);
color: var(--text-color, #ccc);
}
h1 {
font-size: 18px;
margin: 0 0 20px 0;
}
h2 {
font-size: 14px;
margin: 0 0 10px 0;
}
h3 {
font-size: 12px;
margin: 0 0 8px 0;
}
.test-section {
margin-bottom: 20px;
border-left: 2px solid var(--divider-color, #666);
padding-left: 10px;
padding: var(--s3);
}
.button-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
padding: 6px 12px;
border: 1px solid var(--divider-color, #666);
background: var(--card-bg, transparent);
color: var(--text-color, #ccc);
font-size: 12px;
font-family: monospace;
cursor: pointer;
}
button:hover {
background: var(--select-hover-bg, rgba(255, 255, 255, 0.1));
border-color: var(--c-primary, #999);
}
.run-all {
display: block;
width: 100%;
margin-bottom: 20px;
border-color: var(--color-success, #4caf50);
color: var(--color-success, #4caf50);
}
.run-all:hover {
background: var(--color-overlay-light-10, rgba(76, 175, 80, 0.1));
}
.console {
background: var(--card-bg, rgba(0, 0, 0, 0.3));
color: var(--text-color, #ccc);
padding: 10px;
font-size: 11px;
max-height: 200px;
overflow-y: auto;
margin-top: 20px;
border: 1px solid var(--divider-color, #333);
}
.console-entry {
margin: 2px 0;
}
.console-entry.success {
color: var(--color-success, #4caf50);
}
.console-entry.error {
color: var(--color-danger, #f44336);
}
.console-entry.info {
color: var(--text-color-muted, #999);
}
.api-info {
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid var(--divider-color, #333);
gap: var(--s);
}
.api-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 8px;
gap: var(--s-half);
}
.api-item {
padding: 4px 8px;
border: 1px solid var(--divider-color, #444);
font-size: 11px;
color: var(--text-color-muted, #999);
.console {
max-height: 200px;
overflow-y: auto;
}
.console-entry.success {
color: var(--color-success);
}
.console-entry.error {
color: var(--color-danger);
}
.console-entry.info {
color: var(--text-color-muted);
}
</style>
</head>
<body>
<h1>API Test Plugin</h1>
<div class="api-info">
<h3>Available API Methods</h3>
<div class="api-list">
<div class="api-item">getTasks()</div>
<div class="api-item">getArchivedTasks()</div>
<div class="api-item">getCurrentContextTasks()</div>
<div class="api-item">addTask(data)</div>
<div class="api-item">updateTask(id, data)</div>
<div class="api-item">getAllProjects()</div>
<div class="api-item">addProject(data)</div>
<div class="api-item">updateProject(id, data)</div>
<div class="api-item">getAllTags()</div>
<div class="api-item">addTag(data)</div>
<div class="api-item">updateTag(id, data)</div>
<div class="api-item">showSnack(config)</div>
<div class="api-item">notify(config)</div>
<div class="api-item">openDialog(config)</div>
<div class="api-item">persistDataSynced(data)</div>
<div class="api-item">loadSyncedData()</div>
<div class="api-item">registerHook(hook, handler)</div>
<div class="api-item">registerHeaderButton(config)</div>
<div class="api-item">registerMenuEntry(config)</div>
<div class="api-item">registerShortcut(config)</div>
<div class="api-item">setCounter(key, value)</div>
<div class="api-item">getCounter(key)</div>
<div class="api-item">incrementCounter(key, amount)</div>
<div class="api-item">decrementCounter(key, amount)</div>
<div class="api-item">deleteCounter(key)</div>
<div class="api-item">getAllCounters()</div>
</div>
<h3>Available API Methods</h3>
<p class="api-list">
<code>getTasks()</code>
<code>getArchivedTasks()</code>
<code>getCurrentContextTasks()</code>
<code>addTask(data)</code>
<code>updateTask(id, data)</code>
<code>getAllProjects()</code>
<code>addProject(data)</code>
<code>updateProject(id, data)</code>
<code>getAllTags()</code>
<code>addTag(data)</code>
<code>updateTag(id, data)</code>
<code>showSnack(config)</code>
<code>notify(config)</code>
<code>openDialog(config)</code>
<code>persistDataSynced(data)</code>
<code>loadSyncedData()</code>
<code>registerHook(hook, handler)</code>
<code>registerHeaderButton(config)</code>
<code>registerMenuEntry(config)</code>
<code>registerShortcut(config)</code>
<code>setCounter(key, value)</code>
<code>getCounter(key)</code>
<code>incrementCounter(key, amount)</code>
<code>decrementCounter(key, amount)</code>
<code>deleteCounter(key)</code>
<code>getAllCounters()</code>
</p>
<hr />
<button onclick="runAllTests()">Run All API Tests</button>
<hr />
<h2>Data Persistence Tests</h2>
<div class="button-grid">
<button onclick="testPersistData()">Test Save Data</button>
<button onclick="testLoadData()">Test Load Data</button>
</div>
<button
class="run-all"
onclick="runAllTests()"
>
Run All API Tests
</button>
<div class="test-section">
<h2>Data Persistence Tests</h2>
<div class="button-grid">
<button onclick="testPersistData()">Test Save Data</button>
<button onclick="testLoadData()">Test Load Data</button>
</div>
<h2>Task Operations</h2>
<div class="button-grid">
<button onclick="testGetTasks()">Get All Tasks</button>
<button onclick="testGetArchivedTasks()">Get Archived Tasks</button>
<button onclick="testGetContextTasks()">Get Context Tasks</button>
<button onclick="testCreateTask()">Create Test Task</button>
<button onclick="testUpdateTask()">Update Last Task</button>
</div>
<div class="test-section">
<h2>Task Operations</h2>
<div class="button-grid">
<button onclick="testGetTasks()">Get All Tasks</button>
<button onclick="testGetArchivedTasks()">Get Archived Tasks</button>
<button onclick="testGetContextTasks()">Get Context Tasks</button>
<button onclick="testCreateTask()">Create Test Task</button>
<button onclick="testUpdateTask()">Update Last Task</button>
</div>
<h2>Project &amp; Tag Operations</h2>
<div class="button-grid">
<button onclick="testGetProjects()">Get All Projects</button>
<button onclick="testCreateProject()">Create Test Project</button>
<button onclick="testGetTags()">Get All Tags</button>
<button onclick="testCreateTag()">Create Test Tag</button>
</div>
<div class="test-section">
<h2>Project & Tag Operations</h2>
<div class="button-grid">
<button onclick="testGetProjects()">Get All Projects</button>
<button onclick="testCreateProject()">Create Test Project</button>
<button onclick="testGetTags()">Get All Tags</button>
<button onclick="testCreateTag()">Create Test Tag</button>
</div>
<h2>UI Operations</h2>
<div class="button-grid">
<button onclick="testSnackSuccess()">Success Snack</button>
<button onclick="testSnackError()">Error Snack</button>
<button onclick="testSnackCustom()">Info Snack</button>
<button onclick="testNotification()">Show Notification</button>
<button onclick="testDialog()">Open Dialog</button>
</div>
<div class="test-section">
<h2>UI Operations</h2>
<div class="button-grid">
<button onclick="testSnackSuccess()">Success Snack</button>
<button onclick="testSnackError()">Error Snack</button>
<button onclick="testSnackCustom()">Info Snack</button>
<button onclick="testNotification()">Show Notification</button>
<button onclick="testDialog()">Open Dialog</button>
</div>
<h2>Advanced Dialog Examples</h2>
<div class="button-grid">
<button onclick="showTaskPlannerDialog()">Task Planner</button>
<button onclick="showAnalyticsDialog()">Analytics Dashboard</button>
</div>
<div class="test-section">
<h2>Advanced Dialog Examples</h2>
<div class="button-grid">
<button onclick="showTaskPlannerDialog()">Task Planner</button>
<button onclick="showAnalyticsDialog()">Analytics Dashboard</button>
</div>
</div>
<div
<pre
class="console"
id="console"
>
<div class="console-entry info">Console output will appear here...</div>
><span class="console-entry info">Console output will appear here...</span></pre>
<hr />
<h2>UI Kit Component Showcase</h2>
<h3>Headings</h3>
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<h3>Text</h3>
<p>
This is a paragraph with <strong>bold</strong>, <em>italic</em>, and
<a href="#">link</a> styles.
</p>
<p>Inline <code>code</code> is styled automatically.</p>
<h3>Buttons</h3>
<div class="button-grid">
<button>Default</button>
<button class="btn-primary">Primary</button>
<button class="btn-outline">Outline</button>
<button disabled>Disabled</button>
<button
class="btn-primary"
disabled
>
Primary Disabled
</button>
<button
class="btn-outline"
disabled
>
Outline Disabled
</button>
</div>
<h3>Inputs</h3>
<input
type="text"
placeholder="Text input"
/>
<textarea placeholder="Textarea"></textarea>
<select>
<option>Select option 1</option>
<option>Select option 2</option>
<option>Select option 3</option>
</select>
<label><input type="checkbox" /> Checkbox label</label>
<h3>Code Block</h3>
<pre><code>const tasks = await PluginAPI.getTasks();
console.log(tasks.length);</code></pre>
<h3>Table</h3>
<table>
<thead>
<tr>
<th>Method</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>getTasks()</code></td>
<td>Get all active tasks</td>
</tr>
<tr>
<td><code>addTask(data)</code></td>
<td>Create a new task</td>
</tr>
<tr>
<td><code>showSnack(cfg)</code></td>
<td>Show a notification</td>
</tr>
</tbody>
</table>
<h3>Lists</h3>
<ul>
<li>Unordered list item 1</li>
<li>Unordered list item 2</li>
<li>Unordered list item 3</li>
</ul>
<ol>
<li>Ordered list item 1</li>
<li>Ordered list item 2</li>
<li>Ordered list item 3</li>
</ol>
<h3>Cards</h3>
<div class="card">
<h4>Default Card</h4>
<p>A static card with background, shadow, and border.</p>
</div>
<br />
<div class="card card-clickable">
<h4>Clickable Card</h4>
<p>Hover to see the lift effect and primary border highlight.</p>
</div>
<h3>Horizontal Rule</h3>
<hr />
<script>
let lastTaskId = null;
let lastProjectId = null;
@ -223,18 +239,18 @@
// Console logging
function log(message, type = 'info') {
const console = document.getElementById('console');
const entry = document.createElement('div');
const con = document.getElementById('console');
const entry = document.createElement('span');
entry.className = `console-entry ${type}`;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
console.appendChild(entry);
console.scrollTop = console.scrollHeight;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}\n`;
con.appendChild(entry);
con.scrollTop = con.scrollHeight;
}
// Clear console
function clearConsole() {
document.getElementById('console').innerHTML =
'<div class="console-entry info">Console cleared. Starting new test run...</div>';
'<span class="console-entry info">Console cleared. Starting new test run...\n</span>';
}
// Test functions

View file

@ -1,119 +1,79 @@
:root {
/* Map host variables to local variables */
--bg-color: var(--bg);
--text-color: var(--text-color);
--primary-color: var(--c-primary);
/* Map host variables to Pico CSS variables */
--pico-background-color: var(--bg);
--pico-color: var(--text-color);
--pico-primary: var(--c-primary);
--pico-primary-background: var(--c-primary);
--pico-primary-hover: var(--c-accent);
--pico-primary-focus: var(--c-accent);
--pico-card-background-color: var(--card-bg);
--pico-border-color: var(--divider-color);
/* Tighten default control padding to avoid oversized buttons */
--pico-form-element-spacing-vertical: 0.35rem;
--pico-form-element-spacing-horizontal: 0.75rem;
/* Additional Pico overrides for better integration */
--pico-form-element-background-color: var(--bg-darker);
--pico-form-element-border-color: var(--divider-color);
--pico-form-element-color: var(--text-color);
}
/*
* Automations Plugin Styles
* UI Kit provides base element styles (body, headings, buttons, inputs, etc.)
* Only plugin-specific layout and component rules here.
*/
html,
body {
background-color: transparent;
}
/* Force primary button color */
button:not(.outline):not(.secondary):not(.contrast):not(.close-btn) {
background-color: var(--c-primary);
border-color: var(--c-primary);
color: #fff;
/* Responsive equal-column grid (replaces Pico .grid) */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
gap: var(--s);
align-items: center;
}
/* Primary outline buttons */
button.outline:not(.secondary):not(.contrast),
button.outline.secondary {
background-color: transparent !important;
color: var(--c-primary) !important;
border-color: var(--c-primary) !important;
}
button.outline:not(.secondary):not(.contrast):hover,
button.outline.secondary:hover {
background-color: var(--c-primary) !important;
color: #fff !important;
}
/* Make secondary outlines look like primary outlines for cancel buttons */
/* Force warn color for delete buttons (using contrast class) */
button.contrast {
background-color: var(--c-warn);
border-color: var(--c-warn);
color: #fff;
}
button.outline.contrast {
background-color: transparent !important;
border-color: var(--c-warn) !important;
color: var(--c-warn) !important;
}
/* Checkboxes */
input[type='checkbox']:checked {
background-color: var(--c-primary);
border-color: var(--c-primary);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
}
body {
margin: 0;
padding: 0;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
background-color: transparent;
/* Dialog/modal styling (replaces Pico <dialog>/<article>) */
dialog {
border: none;
border-radius: var(--card-border-radius);
background: var(--card-bg);
color: var(--text-color);
}
/* Custom styles to supplement Pico CSS */
/* Override container width */
main.container {
max-width: 960px !important;
}
/* Reduce button height globally */
button,
[role='button'],
input[type='submit'],
input[type='button'],
input[type='reset'] {
min-height: auto;
padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
}
/* Dialog styles */
dialog article {
padding: 0;
max-width: 600px;
width: 100%;
width: calc(100% - var(--s4));
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
dialog article {
padding: var(--s3);
}
dialog article header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--s2);
}
/* Table styles */
table {
width: 100%;
dialog article footer {
margin-top: var(--s3);
padding-top: var(--s2);
border-top: 1px solid var(--divider-color);
}
/* Close button in dialogs */
.close-btn {
background: none;
border: none;
padding: var(--s-half);
font-size: 1.2em;
line-height: 1;
cursor: pointer;
color: var(--text-color-muted);
}
.close-btn:hover {
color: var(--text-color);
background: none;
border: none;
}
/* Figure wrapper for table overflow */
figure {
margin: 0;
overflow-x: auto;
}
.app {
padding: 20px;
padding: var(--s3);
max-width: 800px;
margin: 0 auto;
}
@ -122,27 +82,16 @@ table {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
border-bottom: 1px solid #eee;
padding-bottom: 1rem;
margin-bottom: var(--s3);
border-bottom: 1px solid var(--divider-color);
padding-bottom: var(--s);
}
[data-theme='dark'] .app-header {
border-bottom-color: #444;
}
/* Warning box styles */
.warning-box {
background-color: #fff3cd;
color: #856404;
padding: 1rem;
margin-bottom: 2rem;
border-radius: var(--pico-border-radius);
border: 1px solid #ffeeba;
}
[data-theme='dark'] .warning-box {
background-color: rgba(255, 193, 7, 0.15);
color: #ffca28;
border-color: rgba(255, 193, 7, 0.3);
background-color: color-mix(in srgb, var(--color-warning) 15%, transparent);
color: var(--color-warning);
padding: var(--s);
margin-bottom: var(--s3);
border-radius: var(--card-border-radius);
border: 1px solid color-mix(in srgb, var(--color-warning) 30%, transparent);
}

View file

@ -37,22 +37,6 @@ function App() {
onMount(async () => {
await Promise.all([fetchRules(), fetchData()]);
setIsLoading(false);
// Theme detection
const isDark =
getComputedStyle(document.documentElement).getPropertyValue('--is-dark-theme').trim() === '1';
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
// Optional: Listen for changes if the host app updates the style tag dynamically
// This might require a MutationObserver on the style tag or body if variables change
const observer = new MutationObserver(() => {
const isDarkNow =
getComputedStyle(document.documentElement).getPropertyValue('--is-dark-theme').trim() ===
'1';
document.documentElement.setAttribute('data-theme', isDarkNow ? 'dark' : 'light');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
// Also observe the injected style tag if possible, but documentElement style check is a good start
});
const handleEdit = (rule: AutomationRule) => {
@ -151,9 +135,9 @@ function App() {
return (
<div class="app">
<main class="container">
<main>
{isLoading() ? (
<article aria-busy="true"></article>
<p>Loading...</p>
) : (
<>
<RuleList

View file

@ -58,10 +58,12 @@ export function ActionDialog(props: ActionDialogProps) {
title={props.initialAction ? 'Edit Action' : 'Add Action'}
footer={
<div class="grid">
<button class="outline secondary" onClick={props.onClose}>
<button class="btn-outline" onClick={props.onClose}>
Cancel
</button>
<button onClick={() => props.onSave(action())}>Save</button>
<button class="btn-primary" onClick={() => props.onSave(action())}>
Save
</button>
</div>
}
>
@ -88,9 +90,9 @@ export function ActionDialog(props: ActionDialogProps) {
{action().type === 'webhook' && (
<p
style={{
color: 'var(--pico-del-color)',
'font-size': '0.875rem',
'margin-top': '0.5rem',
color: 'var(--color-warning)',
'font-size': '0.875em',
'margin-top': 'var(--s)',
}}
>
Warning: Your full task data (including title, description, etc.) will be sent to this

View file

@ -38,10 +38,12 @@ export function ConditionDialog(props: ConditionDialogProps) {
title={props.initialCondition ? 'Edit Condition' : 'Add Condition'}
footer={
<div class="grid">
<button class="outline secondary" onClick={props.onClose}>
<button class="btn-outline" onClick={props.onClose}>
Cancel
</button>
<button onClick={() => props.onSave(condition())}>Save</button>
<button class="btn-primary" onClick={() => props.onSave(condition())}>
Save
</button>
</div>
}
>

View file

@ -123,20 +123,20 @@ export function RuleEditor(props: RuleEditorProps) {
<div class="grid">
<div style={{ 'text-align': 'left' }}>
{localRule().id && (
<button class="outline contrast" onClick={() => props.onDelete(localRule())}>
Delete Rule
</button>
<button onClick={() => props.onDelete(localRule())}>Delete Rule</button>
)}
</div>
<div style={{ 'text-align': 'right' }}>
<button
class="outline secondary"
class="btn-outline"
onClick={props.onCancel}
style={{ 'margin-right': '0.5rem' }}
>
Cancel
</button>
<button onClick={saveLocalRule}>Save Rule</button>
<button class="btn-primary" onClick={saveLocalRule}>
Save Rule
</button>
</div>
</div>
}
@ -237,7 +237,7 @@ export function RuleEditor(props: RuleEditorProps) {
</td>
<td style={{ 'text-align': 'right' }}>
<button
class="outline"
class="btn-outline"
onClick={() => {
setEditingConditionIndex(i());
setIsConditionDialogOpen(true);
@ -246,12 +246,7 @@ export function RuleEditor(props: RuleEditorProps) {
>
Edit
</button>
<button
class="outline contrast"
onClick={() => handleRemoveCondition(i())}
>
</button>
<button onClick={() => handleRemoveCondition(i())}></button>
</td>
</tr>
)}
@ -300,7 +295,7 @@ export function RuleEditor(props: RuleEditorProps) {
</td>
<td style={{ 'text-align': 'right' }}>
<button
class="outline"
class="btn-outline"
onClick={() => {
setEditingActionIndex(i());
setIsActionDialogOpen(true);
@ -309,9 +304,7 @@ export function RuleEditor(props: RuleEditorProps) {
>
Edit
</button>
<button class="outline contrast" onClick={() => handleRemoveAction(i())}>
</button>
<button onClick={() => handleRemoveAction(i())}></button>
</td>
</tr>
)}

View file

@ -45,13 +45,19 @@ export function RuleList(props: RuleListProps) {
ref={fileInputRef!}
onChange={handleFileChange}
/>
<button class="outline" onClick={handleImportClick} style={{ 'margin-right': '0.5rem' }}>
<button
class="btn-outline"
onClick={handleImportClick}
style={{ 'margin-right': '0.5rem' }}
>
Import
</button>
<button class="outline" onClick={props.onExport} style={{ 'margin-right': '0.5rem' }}>
<button class="btn-outline" onClick={props.onExport} style={{ 'margin-right': '0.5rem' }}>
Export
</button>
<button onClick={props.onCreate}>+ New Rule</button>
<button class="btn-primary" onClick={props.onCreate}>
+ New Rule
</button>
</div>
</div>
@ -95,15 +101,13 @@ export function RuleList(props: RuleListProps) {
</td>
<td style={{ 'text-align': 'right', 'white-space': 'nowrap' }}>
<button
class="outline"
class="btn-outline"
onClick={() => props.onEdit(rule)}
style={{ 'margin-right': '0.5rem' }}
>
Edit
</button>
<button class="outline contrast" onClick={() => props.onDelete(rule)}>
Delete
</button>
<button onClick={() => props.onDelete(rule)}>Delete</button>
</td>
</tr>
)}

View file

@ -5,27 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Solid.js Boilerplate Plugin</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Open Sans', sans-serif;
background: var(--bg-color, #ffffff);
color: var(--text-color, #333333);
}
#root {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
body {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
}
}
</style>
</head>
<body>

View file

@ -1,5 +1,4 @@
import { render } from 'solid-js/web';
import '@picocss/pico/css/pico.classless.css';
import App from './app/App';
// Mount the Solid.js app

View file

@ -1,51 +1,14 @@
/*
* Procrastination Buster Plugin Styles
* Simplified structure following KISS principles
* UI Kit provides base element styles (body, headings, buttons, etc.)
* Only plugin-specific layout rules here.
*/
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Open Sans', sans-serif;
background: transparent;
color: var(--text-color);
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
padding: 1rem;
}
/* Common card styles */
.card {
background: var(--card-bg);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 1rem;
text-align: left;
border: 2px solid var(--extra-border-color);
}
.card-clickable:hover {
transition: transform 0.2s;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
border-color: var(--c-primary);
cursor: pointer;
}
/* Common text styles */
.text-muted {
color: var(--text-color-muted);
}
.text-primary {
color: var(--c-primary);
padding: var(--s2);
}
.header {
@ -56,24 +19,6 @@ body {
.header h1 {
margin: 0;
font-size: 1.5rem;
color: var(--c-primary);
}
.back-button {
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--c-primary);
color: var(--c-primary);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.back-button:hover {
background: var(--c-primary);
color: white;
}
.main {
@ -84,34 +29,19 @@ body {
.intro {
text-align: center;
margin-bottom: 2rem;
}
.intro h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.intro p {
/* Uses text-muted class */
margin-bottom: var(--s3);
}
/* Blocker Selector */
.blocker-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
margin-top: 2rem;
}
.blocker-card h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
gap: var(--s);
margin-top: var(--s3);
}
.blocker-card p {
margin: 0;
font-size: 0.9rem;
}
/* Strategy List */
@ -121,12 +51,12 @@ body {
}
.selected-type {
padding: 1.5rem;
padding: var(--s3);
text-align: center;
}
.selected-type h2 {
margin: 0 0 0.5rem 0;
margin: 0 0 var(--s) 0;
}
.emotion {
@ -137,18 +67,18 @@ body {
.strategy-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
gap: var(--s);
}
.strategy-item {
padding: 1rem;
padding: var(--s2);
}
.strategy-content {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
gap: var(--s2);
flex-wrap: wrap;
}
@ -157,82 +87,19 @@ body {
flex: 1;
}
/* Strategy Actions */
.strategy-action-btn {
padding: 6px 12px;
border: 1px solid var(--c-primary);
background: transparent;
color: var(--c-primary);
border-radius: 4px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
margin-left: auto;
}
.strategy-action-btn:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Simple page transition */
.page-fade {
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Info button */
.info-button {
margin-top: 1rem;
padding: 0.5rem 1rem;
background: transparent;
border: 1px solid var(--extra-border-color);
color: var(--text-color-muted);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.info-button:hover {
border-color: var(--c-primary);
color: var(--c-primary);
}
/* Info content */
.info-content {
max-width: 700px;
margin: 0 auto;
padding: 1rem;
padding: var(--s2);
}
.info-content section {
margin-bottom: 2rem;
margin-bottom: var(--s3);
}
.info-content h3 {
color: var(--c-primary);
margin-bottom: 0.75rem;
}
.info-content p {
line-height: 1.6;
margin-bottom: 1rem;
}
.info-content ul {
line-height: 1.8;
padding-left: 1.5rem;
margin-bottom: 1rem;
}
/* Procrastination graph */
@ -240,22 +107,21 @@ body {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin: 1.5rem 0;
gap: var(--s);
margin: var(--s3) 0;
flex-wrap: wrap;
}
.graph-item {
padding: 0.5rem 1rem;
padding: var(--s) var(--s2);
background: var(--card-bg);
border: 1px solid var(--extra-border-color);
border-radius: 4px;
border-radius: var(--card-border-radius);
text-align: center;
font-size: 0.9rem;
font-size: 0.9em;
}
.sync-icon {
font-size: 1.2rem;
color: var(--text-color-muted);
}
@ -263,22 +129,5 @@ body {
.action-buttons {
display: flex;
justify-content: center;
margin-top: 2rem;
}
.primary-button {
padding: 0.75rem 2rem;
background: var(--c-primary);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
font-family: inherit;
transition: all 0.2s;
}
.primary-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
margin-top: var(--s3);
}

View file

@ -80,7 +80,7 @@ const App: Component = () => {
<Show when={currentView() !== 'home'}>
<header class="header page-fade">
<button
class="back-button"
class="btn-outline"
onClick={handleBack}
>
{backButton()}
@ -95,7 +95,7 @@ const App: Component = () => {
<h2>{homeTitle()}</h2>
<p class="text-muted">{homeSubtitle()}</p>
<button
class="info-button"
class="btn-outline"
onClick={() => setCurrentView('info')}
>
{learnMoreButton()}
@ -146,7 +146,7 @@ const App: Component = () => {
<p class="strategy-text">{text}</p>
<Show when={hasAction}>
<button
class="strategy-action-btn"
class="btn-primary"
onClick={() => sendPluginMessage('START_POMODORO')}
title={actionButtonTitle()}
>

View file

@ -129,7 +129,7 @@ export const ProcrastinationInfo: Component<ProcrastinationInfoProps> = (props)
<div class="action-buttons">
<button
class="primary-button"
class="btn-primary"
onClick={props.onBackToWork}
>
{backToWork()}

View file

@ -8,175 +8,62 @@
/>
<title>sync.md Configuration</title>
<style>
/* Reset and base styles */
:root {
--bg-primary: #fff;
--bg-secondary: #f5f5f5;
--bg-tertiary: #e0e0e0;
--text-primary: #333;
--text-secondary: #666;
--text-muted: #999;
--border-color: #ddd;
--border-color-focus: #2196f3;
--accent-primary: #2196f3;
--accent-primary-hover: #1976d2;
--success-bg: #e8f5e9;
--success-text: #2e7d32;
--success-border: #c8e6c9;
--error-bg: #ffebee;
--error-text: #c62828;
--error-border: #ffcdd2;
--info-bg: #e3f2fd;
--info-text: #1565c0;
--info-border: #bbdefb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #2a2a2a;
--bg-secondary: #333;
--bg-tertiary: #444;
--text-primary: #e0e0e0;
--text-secondary: #ccc;
--text-muted: #aaa;
--border-color: #444;
--border-color-focus: #4a9eff;
--accent-primary: #1976d2;
--accent-primary-hover: #1565c0;
--success-bg: #1b3a1b;
--success-text: #81c784;
--success-border: #2e5f2e;
--error-bg: #4a1414;
--error-text: #ef5350;
--error-border: #6f2020;
--info-bg: #1e3a5f;
--info-text: #64b5f6;
--info-border: #2962a0;
}
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
/*
* sync.md Plugin Styles
* UI Kit provides base element styles (body, headings, buttons, inputs, etc.)
* Only plugin-specific layout and component rules here.
*/
body {
font-family: 'Open Sans', 'Helvetica Neue', Arial, sans-serif;
font-size: 14px;
line-height: 1.5;
color: var(--text-primary);
padding: var(--s3);
background: transparent;
padding: 20px;
}
/* Container */
.container {
max-width: 600px;
margin: 0 auto;
}
/* Header */
h1 {
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.subtitle {
color: var(--text-secondary);
margin-bottom: 24px;
color: var(--text-color-muted);
margin-bottom: var(--s3);
}
/* Form elements */
.form-group {
margin-bottom: 20px;
margin-bottom: var(--s3);
}
label {
display: block;
font-weight: 500;
margin-bottom: 6px;
color: var(--text-secondary);
}
input[type='text'],
select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
background: var(--bg-primary);
color: var(--text-primary);
transition: border-color 0.2s;
}
input[type='text']:focus,
select:focus {
outline: none;
border-color: var(--border-color-focus);
background: var(--bg-secondary);
margin-bottom: var(--s-half);
color: var(--text-color-muted);
}
.help-text {
font-size: 12px;
color: var(--text-muted);
margin-top: 4px;
font-size: 0.85em;
color: var(--text-color-muted);
margin-top: var(--s-half);
}
/* Buttons */
.button-group {
display: flex;
gap: 12px;
margin-top: 24px;
gap: var(--s);
margin-top: var(--s3);
}
.warning {
border: 1px solid var(--accent-primary);
border-radius: 4px;
padding: 4px 8px;
border: 1px solid var(--c-primary);
border-radius: var(--card-border-radius);
padding: var(--s-half) var(--s);
font-weight: bold;
}
button {
padding: 10px 20px;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: var(--bg-secondary);
color: var(--text-primary);
transition: all 0.2s;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: var(--accent-primary);
border-color: var(--accent-primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--accent-primary-hover);
border-color: var(--accent-primary-hover);
}
.btn-secondary:hover:not(:disabled) {
background: var(--bg-tertiary);
border-color: var(--border-color);
}
/* Status messages */
.status {
padding: 12px 16px;
border-radius: 6px;
margin-top: 20px;
padding: var(--s) var(--s2);
border-radius: var(--card-border-radius);
margin-top: var(--s3);
display: none;
}
@ -185,21 +72,21 @@
}
.status.info {
background: var(--info-bg);
color: var(--info-text);
border: 1px solid var(--info-border);
background: color-mix(in srgb, var(--c-primary) 15%, transparent);
color: var(--c-primary);
border: 1px solid color-mix(in srgb, var(--c-primary) 30%, transparent);
}
.status.success {
background: var(--success-bg);
color: var(--success-text);
border: 1px solid var(--success-border);
background: color-mix(in srgb, var(--color-success) 15%, transparent);
color: var(--color-success);
border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent);
}
.status.error {
background: var(--error-bg);
color: var(--error-text);
border: 1px solid var(--error-border);
background: color-mix(in srgb, var(--color-danger) 15%, transparent);
color: var(--color-danger);
border: 1px solid color-mix(in srgb, var(--color-danger) 30%, transparent);
}
/* Loading spinner */
@ -207,11 +94,11 @@
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid var(--border-color);
border-top: 2px solid var(--border-color-focus);
border: 2px solid var(--divider-color);
border-top: 2px solid var(--c-primary);
border-radius: 50%;
animation: spin 1s linear infinite;
margin-left: 8px;
margin-left: var(--s);
vertical-align: middle;
}
@ -234,17 +121,8 @@
right: 8px;
top: 50%;
transform: translateY(-50%);
padding: 6px 12px;
font-size: 12px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
color: var(--text-primary);
}
.test-file-btn:hover {
background: var(--bg-secondary);
padding: var(--s-quarter) var(--s);
font-size: 0.85em;
}
</style>
</head>

View file

@ -8,48 +8,33 @@
/>
<title>Yesterday's Tasks</title>
<style>
/* Theme vars + UI Kit are injected automatically before this style block.
Only plugin-specific layout rules are needed here. */
body {
font-family: 'Open Sans', sans-serif;
margin: 0;
padding: 20px;
color: var(--text-color);
padding: var(--s3);
}
/* Light mode (default) */
:root {
--text-color: var(--text-color);
--border-color: var(--separator-color);
--primary-color: var(--c-primary);
}
/* Dark mode */
.container {
max-width: 800px;
margin: 0 auto;
}
h1 {
margin-bottom: 10px;
font-size: 14px;
}
.date {
font-size: 14px;
margin-bottom: 20px;
opacity: 0.7;
margin-bottom: var(--s3);
color: var(--text-color-muted);
}
.loading,
.no-tasks {
text-align: center;
padding: 40px;
color: var(--text-secondary);
padding: var(--s4);
color: var(--text-color-muted);
}
.task-item {
padding: 8px 0;
border-bottom: 1px solid var(--border-color);
padding: var(--s) 0;
border-bottom: 1px solid var(--divider-color);
display: flex;
align-items: flex-start;
}
@ -60,11 +45,10 @@
.task-title {
flex: 1;
margin-right: 16px;
margin-right: var(--s2);
}
.task-time {
font-weight: 400;
white-space: nowrap;
min-width: 60px;
text-align: right;
@ -72,11 +56,11 @@
.project-tag {
display: inline-block;
padding: 2px 8px;
margin-left: 8px;
font-size: 12px;
opacity: 0.7;
border: 1px solid var(--border-color);
padding: var(--s-quarter) var(--s);
margin-left: var(--s);
font-size: 0.85em;
color: var(--text-color-muted);
border: 1px solid var(--divider-color);
border-radius: 12px;
}
</style>

View file

@ -2,6 +2,7 @@ import { PluginBridgeService } from '../plugin-bridge.service';
import { PluginBaseCfg, PluginManifest } from '../plugin-api.model';
import { PluginIframeMessageType } from '@super-productivity/plugin-api';
import { PluginLog } from '../../core/log';
import { PLUGIN_UI_KIT_CSS } from './plugin-ui-kit.css';
/**
* Simplified plugin iframe utilities following KISS principles.
@ -59,7 +60,7 @@ export const createPluginCssInjection = (): string => {
--scrollbar-thumb: ${getVar('--scrollbar-thumb')};
--scrollbar-thumb-hover: ${getVar('--scrollbar-thumb-hover')};
--scrollbar-track: ${getVar('--scrollbar-track')};
/* Shadow system */
--whiteframe-shadow-1dp: ${getVar('--whiteframe-shadow-1dp')};
--whiteframe-shadow-2dp: ${getVar('--whiteframe-shadow-2dp')};
@ -69,7 +70,7 @@ export const createPluginCssInjection = (): string => {
--whiteframe-shadow-8dp: ${getVar('--whiteframe-shadow-8dp')};
--whiteframe-shadow-12dp: ${getVar('--whiteframe-shadow-12dp')};
--whiteframe-shadow-24dp: ${getVar('--whiteframe-shadow-24dp')};
/* Spacing system */
--s: ${getVar('--s')};
--s-quarter: ${getVar('--s-quarter')};
@ -77,7 +78,7 @@ export const createPluginCssInjection = (): string => {
--s2: ${getVar('--s2')};
--s3: ${getVar('--s3')};
--s4: ${getVar('--s4')};
/* Transition system */
--transition-duration-xs: ${getVar('--transition-duration-xs')};
--transition-duration-s: ${getVar('--transition-duration-s')};
@ -85,7 +86,10 @@ export const createPluginCssInjection = (): string => {
--transition-duration-l: ${getVar('--transition-duration-l')};
--transition-standard: ${getVar('--transition-standard')};
--ani-standard-timing: ${getVar('--ani-standard-timing')};
/* Font stack */
--font-primary-stack: ${getVar('--font-primary-stack')};
/* Task-related variables */
--task-first-line-min-height: ${getVar('--task-first-line-min-height')};
--task-icon-default-opacity: ${getVar('--task-icon-default-opacity')};
@ -113,6 +117,7 @@ export const createPluginCssInjection = (): string => {
body {
background: transparent;
color: var(--text-color);
font-family: var(--font-primary-stack);
}
/* Custom scrollbar styles for plugins */
@ -385,16 +390,19 @@ const activeBlobUrls = new Set<string>();
export const createPluginIframeUrl = (config: PluginIframeConfig): string => {
const apiScript = createPluginApiScript(config);
const cssInjection = createPluginCssInjection();
const fullCssInjection =
cssInjection + (config.manifest.uiKit !== false ? PLUGIN_UI_KIT_CSS : '');
// Inject CSS in head
// Inject CSS at start of head so plugin styles come later and win by source order
let html = config.indexHtml;
const headEnd = html.toLowerCase().lastIndexOf('</head>');
const headMatch = html.match(/<head[^>]*>/i);
if (headEnd !== -1) {
html = html.slice(0, headEnd) + cssInjection + html.slice(headEnd);
if (headMatch) {
const insertPos = headMatch.index! + headMatch[0].length;
html = html.slice(0, insertPos) + fullCssInjection + html.slice(insertPos);
} else {
// If no head tag, inject at beginning
html = cssInjection + html;
html = fullCssInjection + html;
}
// Inject API script before closing body tag

View file

@ -0,0 +1,189 @@
/**
* Lightweight CSS reset for plugin iframes.
* Auto-styles basic HTML elements to match the host app theme.
* Injected before plugin styles so plugin CSS wins by source order.
*/
export const PLUGIN_UI_KIT_CSS = `
<style id="sp-ui-kit">
*, *::before, *::after {
box-sizing: border-box;
}
body {
font-size: 14px;
line-height: 1.4;
margin: 0;
}
h1 { font-size: 1.8em; font-weight: 700; margin: 0 0 var(--s2) 0; }
h2 { font-size: 1.4em; font-weight: 700; margin: 0 0 var(--s2) 0; }
h3 { font-size: 1.2em; font-weight: 700; margin: 0 0 var(--s) 0; }
h4 { font-size: 1.05em; font-weight: 700; margin: 0 0 var(--s) 0; }
h5 { font-size: 0.95em; font-weight: 400; margin: 0 0 var(--s) 0; }
h6 { font-size: 0.85em; font-weight: 400; margin: 0 0 var(--s) 0; }
p {
margin: 0 0 var(--s2) 0;
line-height: 1.5;
}
button {
background: var(--card-bg);
color: var(--text-color);
border: 1px solid var(--divider-color);
border-radius: var(--card-border-radius);
padding: var(--s-half) var(--s2);
cursor: pointer;
font-family: inherit;
font-size: inherit;
transition: var(--transition-standard);
}
button:hover {
background: var(--select-hover-bg);
border-color: var(--c-primary);
}
button:active {
filter: brightness(0.92);
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
button.btn-primary {
background: var(--c-primary);
border-color: var(--c-primary);
color: #fff;
}
button.btn-primary:hover {
filter: brightness(1.12);
}
button.btn-outline {
background: transparent;
border-color: var(--c-primary);
color: var(--c-primary);
}
button.btn-outline:hover {
background: var(--c-primary);
color: #fff;
}
input, textarea, select {
background: var(--bg);
color: var(--text-color);
border: 1px solid var(--divider-color);
border-radius: var(--card-border-radius);
padding: var(--s) var(--s2);
font-family: inherit;
font-size: inherit;
transition: var(--transition-standard);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--c-primary);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--c-primary) 25%, transparent);
}
input:disabled, textarea:disabled, select:disabled {
opacity: 0.5;
cursor: not-allowed;
}
label {
cursor: pointer;
}
a {
color: var(--c-primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
ul, ol {
padding-left: var(--s3);
margin: 0 0 var(--s2) 0;
}
table {
border-collapse: collapse;
width: 100%;
}
th, td {
padding: var(--s) var(--s2);
border-bottom: 1px solid var(--divider-color);
text-align: left;
}
hr {
border: none;
border-top: 1px solid var(--divider-color);
margin: var(--s2) 0;
}
code {
background: var(--bg-darker);
padding: var(--s-quarter) var(--s-half);
border-radius: 2px;
font-size: 0.9em;
}
pre {
background: var(--bg-darker);
padding: var(--s2);
border-radius: var(--card-border-radius);
overflow-x: auto;
}
pre > code {
background: none;
padding: 0;
}
.card {
background: var(--card-bg);
border-radius: var(--card-border-radius);
box-shadow: var(--whiteframe-shadow-2dp);
padding: var(--s2);
text-align: left;
border: 2px solid var(--extra-border-color);
}
.card-clickable:hover {
transition: transform 0.2s;
transform: translateY(-2px);
box-shadow: var(--whiteframe-shadow-4dp);
border-color: var(--c-primary);
cursor: pointer;
}
::selection {
background: color-mix(in srgb, var(--c-primary) 30%, transparent);
}
::placeholder {
color: var(--text-color-muted);
}
/* Utility classes */
.text-muted { color: var(--text-color-muted); }
.text-primary { color: var(--c-primary); }
/* Page transition */
.page-fade { animation: fadeIn 0.3s ease; }
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
`;