picocrank 1.23.1 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/AGENTS.md +2 -0
  2. package/docs/filter-query-v1.md +180 -0
  3. package/logo.svg +7 -7
  4. package/package.json +7 -5
  5. package/public/themes/README.md +19 -2
  6. package/public/themes/index.json +1 -5
  7. package/styles/dark-theme.css +16 -1
  8. package/supplemental-themes/README.md +9 -0
  9. package/supplemental-themes/catppuccin-latte-frappe/theme.css +276 -0
  10. package/supplemental-themes/dracula-alucard/theme.css +258 -0
  11. package/supplemental-themes/gruvbox-dark-light/theme.css +264 -0
  12. package/supplemental-themes/index.json +17 -0
  13. package/{public/themes → supplemental-themes}/waffles/theme.css +99 -0
  14. package/vite.config.js +91 -0
  15. package/vue/components/FormField.vue +69 -1
  16. package/vue/components/Header.vue +3 -3
  17. package/vue/components/Login.vue +1 -3
  18. package/vue/components/Section.vue +45 -14
  19. package/vue/components/Sidebar.vue +2 -1
  20. package/vue/components/SidebarNavLink.vue +6 -0
  21. package/vue/components/Table.vue +1113 -73
  22. package/vue/components/TableColumnFilterPopover.vue +263 -44
  23. package/vue/components/TableColumnOptionsPopover.vue +1116 -0
  24. package/vue/components/ThemeSwitcher.vue +119 -0
  25. package/vue/components/TopBar.vue +2 -2
  26. package/vue/composables/tableColumnPresets.js +296 -0
  27. package/vue/composables/tableFilterQuery.js +202 -0
  28. package/vue/composables/tableFilters.js +167 -3
  29. package/vue/composables/useCustomTheme.js +167 -24
  30. package/vue/data/examplePeople.js +2 -6
  31. package/vue/examples/App.vue +1 -1
  32. package/vue/examples/FormExample.vue +40 -6
  33. package/vue/examples/LoginExample.vue +1 -3
  34. package/vue/examples/TableExample.vue +329 -11
  35. package/vue/examples/TableRemoteExample.vue +112 -4
  36. package/vue/examples/ThemePreview.vue +17 -31
  37. package/public/themes/catppuccin/theme.css +0 -195
  38. package/public/themes/dracula/theme.css +0 -181
package/AGENTS.md CHANGED
@@ -5,6 +5,8 @@ PicoCrank is a Vue component library demo built on the [Femtocrank](https://gith
5
5
  ## UI patterns
6
6
 
7
7
  - [Buttons with icons](docs/buttons-with-icons.md) — Femtocrank `.inline-icon`, Hugeicons, karma variants (icon + text and icon-only).
8
+ - [Table filterQuery v1](docs/filter-query-v1.md) — canonical remote filter wire format for `Table` / `fetchRows`.
9
+ - Drop-in themes: `ThemeSwitcher` + `useCustomTheme()`; supplemental themes (Catppuccin, Dracula / Alucard, Gruvbox, waffles) bundled by default, loaded only when `includeSupplementalThemes` is set.
8
10
 
9
11
  ## Examples app
10
12
 
@@ -0,0 +1,180 @@
1
+ # Table filterQuery v1
2
+
3
+ Canonical wire format for PicoCrank `Table` column filters. Use **`filterQuery`** in remote integrations (`fetchRows`, server persistence). The table builds this object from UI state; client-side tables apply it with `applyFilterQuery`.
4
+
5
+ Implementation: [`vue/composables/tableFilterQuery.js`](../vue/composables/tableFilterQuery.js).
6
+
7
+ Live reference: [`vue/examples/TableRemoteExample.vue`](../vue/examples/TableRemoteExample.vue) (`fetchRows` logs `query.filterQuery`).
8
+
9
+ ## Query payload
10
+
11
+ Remote `fetchRows` and `@query-change` (when `fetchRows` is set) receive:
12
+
13
+ ```ts
14
+ {
15
+ page: number
16
+ pageSize: number
17
+ sortBy: string | null
18
+ sortDir: 'asc' | 'desc'
19
+ filterQuery: FilterQueryV1
20
+ }
21
+ ```
22
+
23
+ > **Deprecation:** In **remote mode**, `filters` is **not** included in the query object. Read `filterQuery` only. Local (non-remote) `@query-change` still emits `filters` for backward compatibility; prefer `filterQuery` for new code.
24
+
25
+ ## FilterQueryV1 shape
26
+
27
+ ```json
28
+ {
29
+ "version": 1,
30
+ "match": "all",
31
+ "columns": [
32
+ {
33
+ "key": "name",
34
+ "match": "all",
35
+ "filters": [
36
+ {
37
+ "id": "filter-…",
38
+ "name": "Managers",
39
+ "enabled": true,
40
+ "type": "text",
41
+ "operator": "contains",
42
+ "value": "Alice"
43
+ }
44
+ ]
45
+ }
46
+ ]
47
+ }
48
+ ```
49
+
50
+ | Field | Type | Description |
51
+ |-------|------|-------------|
52
+ | `version` | `1` | Schema version. Always `1` for v1. |
53
+ | `match` | `'all' \| 'any'` | How to combine **columns**. `'all'` = every column group must match (AND). `'any'` = at least one column group matches (OR). UI-built queries use `'all'`. |
54
+ | `columns` | array | Non-empty list of per-column filter groups. Unknown or non-filterable columns are dropped on normalize. |
55
+ | `columns[].key` | string | Header `key`. |
56
+ | `columns[].match` | `'all' \| 'any'` | How to combine **filters within the column**. `'all'` = every enabled filter must match (AND). `'any'` = at least one enabled filter matches (OR). UI-built queries use `'all'`. |
57
+ | `columns[].filters` | array | One or more filter entries (see below). |
58
+
59
+ ### Filter entry fields
60
+
61
+ | Field | Required | Description |
62
+ |-------|----------|-------------|
63
+ | `type` | yes | `'text'`, `'number'`, `'boolean'`, or `'select'`. Usually matches header `filterType`. |
64
+ | `enabled` | no | Default `true`. Disabled entries are ignored when applying. |
65
+ | `id` | no | Stable id for UI toggling; safe to ignore on the server. |
66
+ | `name` | no | Optional display label from the filter popover. |
67
+ | `operator` | text/number | See operators below. |
68
+ | `value` | text/number/boolean | Comparison value. |
69
+ | `values` | select | Array of selected values (OR within the select filter: row value must match **any** selected value). |
70
+
71
+ Inactive or invalid entries (empty text, invalid number, empty select) are omitted when the query is built from UI state.
72
+
73
+ ## Filter types and operators
74
+
75
+ ### Text (`type: 'text'`)
76
+
77
+ Case-insensitive comparison on stringified cell values.
78
+
79
+ | `operator` | Meaning |
80
+ |------------|---------|
81
+ | `contains` | Cell contains substring (default). |
82
+ | `equals` | Cell equals string (case-insensitive). |
83
+ | `startsWith` | Cell starts with string. |
84
+ | `empty` | Cell is empty / whitespace only (`value` ignored). |
85
+
86
+ ### Number (`type: 'number'`)
87
+
88
+ Numeric comparison; non-numeric cells do not match.
89
+
90
+ | `operator` | Meaning |
91
+ |------------|---------|
92
+ | `eq` | Equal (default). |
93
+ | `gt` | Greater than. |
94
+ | `gte` | Greater or equal. |
95
+ | `lt` | Less than. |
96
+ | `lte` | Less or equal. |
97
+
98
+ ### Boolean (`type: 'boolean'`)
99
+
100
+ | `value` | Meaning |
101
+ |---------|---------|
102
+ | `true` | Cell is truthy. |
103
+ | `false` | Cell is falsy. |
104
+
105
+ ### Select (`type: 'select'`)
106
+
107
+ | Field | Meaning |
108
+ |-------|---------|
109
+ | `values` | Non-empty array. Row matches if cell value equals **any** entry (strict or string-coerced equality). |
110
+
111
+ ## Matching semantics (summary)
112
+
113
+ 1. For each **column** in `columns`, evaluate enabled filters using that column’s `match` (`all` / `any`).
114
+ 2. Combine column results using top-level `match` (`all` / `any`).
115
+ 3. Columns with no active filters are ignored.
116
+
117
+ Default UI behavior: **AND** across columns, **AND** across filters on the same column.
118
+
119
+ ## JavaScript helpers
120
+
121
+ ```js
122
+ import {
123
+ buildFilterQuery,
124
+ normalizeFilterQuery,
125
+ applyFilterQuery,
126
+ filterQueryToFilters,
127
+ countActiveFilterQuery,
128
+ isFilterQueryActive,
129
+ cloneFilterQuery,
130
+ FILTER_QUERY_VERSION,
131
+ } from '../composables/tableFilterQuery.js'
132
+ ```
133
+
134
+ | Function | Purpose |
135
+ |----------|---------|
136
+ | `buildFilterQuery(filters, headers, rows)` | UI storage `{ [columnKey]: entries[] }` → `FilterQueryV1`. |
137
+ | `normalizeFilterQuery(filterQuery, headers, rows)` | Validate, drop unknown columns, normalize filter specs. |
138
+ | `applyFilterQuery(rows, headers, filterQuery)` | Client-side row filtering (same semantics as the table). |
139
+ | `filterQueryToFilters(filterQuery, headers, rows)` | Convert back to UI storage shape (e.g. after loading from an API). |
140
+ | `countActiveFilterQuery(filterQuery, headers, rows)` | Count enabled, active filters. |
141
+
142
+ ## Remote `fetchRows` example
143
+
144
+ ```js
145
+ async function fetchRows(query, { signal }) {
146
+ // query.filterQuery — use this, not query.filters
147
+ const res = await api.listRows({
148
+ page: query.page,
149
+ pageSize: query.pageSize,
150
+ sortBy: query.sortBy,
151
+ sortDir: query.sortDir,
152
+ filterQuery: query.filterQuery,
153
+ signal,
154
+ })
155
+ return { rows: res.rows, total: res.total }
156
+ }
157
+ ```
158
+
159
+ To mirror client semantics in Node without reimplementing operators, reuse `applyFilterQuery` on an in-memory dataset (as in `TableRemoteExample`), or port the logic to your query layer.
160
+
161
+ ## Layout presets vs filterQuery
162
+
163
+ Saved layouts (localStorage or `layoutPresets` callbacks) store **`filters`** (UI storage shape) inside `state`, not `filterQuery`. When a preset is applied, the table rebuilds `filterQuery` automatically. Server-side layout persistence can store either:
164
+
165
+ - **`state.filters`** — round-trips the table UI exactly, or
166
+ - **`filterQuery`** — if you control load/save; convert with `filterQueryToFilters` before `setFilters` if needed.
167
+
168
+ ## Header configuration
169
+
170
+ Set `filterType` on headers for remote tables (empty `:data` cannot infer types):
171
+
172
+ ```js
173
+ { key: 'age', label: 'Age', filterable: true, filterType: 'number' }
174
+ { key: 'city', label: 'City', filterable: true, filterType: 'select', filterOptions: […] }
175
+ ```
176
+
177
+ ## Versioning
178
+
179
+ - **v1** (`version: 1`): current schema.
180
+ - Servers should ignore unknown top-level fields and reject unsupported `version` values explicitly if strict validation is required.
package/logo.svg CHANGED
@@ -8,8 +8,8 @@
8
8
  sodipodi:docname="logo.svg"
9
9
  inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
10
10
  inkscape:export-filename="logo.png"
11
- inkscape:export-xdpi="2048"
12
- inkscape:export-ydpi="2048"
11
+ inkscape:export-xdpi="96"
12
+ inkscape:export-ydpi="96"
13
13
  xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14
14
  xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15
15
  xmlns="http://www.w3.org/2000/svg"
@@ -26,12 +26,12 @@
26
26
  inkscape:pagecheckerboard="1"
27
27
  inkscape:deskcolor="#d1d1d1"
28
28
  inkscape:zoom="2"
29
- inkscape:cx="114"
29
+ inkscape:cx="113.75"
30
30
  inkscape:cy="158.25"
31
- inkscape:window-width="3840"
32
- inkscape:window-height="2126"
33
- inkscape:window-x="2160"
34
- inkscape:window-y="2194"
31
+ inkscape:window-width="2160"
32
+ inkscape:window-height="3806"
33
+ inkscape:window-x="6000"
34
+ inkscape:window-y="514"
35
35
  inkscape:window-maximized="1"
36
36
  inkscape:current-layer="g2" />
37
37
  <g
package/package.json CHANGED
@@ -1,10 +1,12 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.23.1",
3
+ "version": "1.25.0",
4
4
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
5
  "main": "vite.config.js",
6
6
  "scripts": {
7
+ "dev": "vite",
7
8
  "build": "vite build --base './'",
9
+ "build:minimal": "VITE_INCLUDE_SUPPLEMENTAL_THEMES=false vite build --base './'",
8
10
  "preview": "vite preview",
9
11
  "test": "echo \"Error: no test specified\" && exit 1"
10
12
  },
@@ -18,12 +20,12 @@
18
20
  "url": "https://github.com/jamesread/picocrank/issues"
19
21
  },
20
22
  "dependencies": {
21
- "@hugeicons/core-free-icons": "^4.2.3",
22
- "@hugeicons/vue": "^1.0.7",
23
+ "@hugeicons/core-free-icons": "^4.3.0",
24
+ "@hugeicons/vue": "^1.0.8",
23
25
  "@vitejs/plugin-vue": "^6.0.8",
24
- "femtocrank": "^2.5.3",
26
+ "femtocrank": "^2.6.0",
25
27
  "unplugin-vue-components": "^32.1.0",
26
- "vite": "^8.2.1",
28
+ "vite": "^8.2.2",
27
29
  "vue": "^3.5.41",
28
30
  "vue-router": "^5.2.0"
29
31
  },
@@ -1,6 +1,8 @@
1
1
  # Drop-in themes
2
2
 
3
- Themes are **not** part of the PicoCrank Vite/JS CSS bundle. Drop a folder here and restart the dev server (or rebuild) to make it discoverable.
3
+ App themes are **not** part of the PicoCrank Vite/JS CSS bundle. Drop a folder under `public/themes/` and restart the dev server (or rebuild) to make it discoverable.
4
+
5
+ **Supplemental themes** (Catppuccin Latte / Frappé, Dracula / Alucard, Gruvbox (Dark+Light), waffles) live in `supplemental-themes/` at the repo root. They are **included in the default build** but are **not listed or loaded** unless the app sets `includeSupplementalThemes: true` on `ThemeSwitcher` or `useCustomTheme()`.
4
6
 
5
7
  ## Layout
6
8
 
@@ -24,6 +26,21 @@ body {
24
26
  }
25
27
  ```
26
28
 
29
+ For supplemental themes, use `/supplemental-themes/<name>/…`.
30
+
27
31
  ## Switching
28
32
 
29
- The examples app uses `useCustomTheme()` to list themes from `index.json` and apply the selected `theme.css` with `@import … layer(theme)`.
33
+ Use `ThemeSwitcher` or `useCustomTheme()`:
34
+
35
+ ```vue
36
+ <!-- App themes only (default) -->
37
+ <ThemeSwitcher />
38
+
39
+ <!-- Also list bundled supplemental themes -->
40
+ <ThemeSwitcher :include-supplemental-themes="true" />
41
+ ```
42
+
43
+ - `includeSupplementalThemes` (default `false`) — list and load supplemental themes when present.
44
+ - `VITE_INCLUDE_SUPPLEMENTAL_THEMES=false` — omit supplemental theme files from `dist` (`npm run build:minimal`).
45
+
46
+ Light/dark mode (header sun/moon) is separate from drop-in themes.
@@ -1,7 +1,3 @@
1
1
  {
2
- "themes": [
3
- "catppuccin",
4
- "dracula",
5
- "waffles"
6
- ]
2
+ "themes": []
7
3
  }
@@ -5,6 +5,7 @@ html[data-theme="dark"] {
5
5
  --border-color: #595959;
6
6
  --hover-background-color: #1d345c;
7
7
  --hover-text-color: #fff;
8
+ --text-color: #ddd;
8
9
  --header-hover-background-color: #1d345c;
9
10
  --standout-bg-color: #1f1f1f;
10
11
  --disabled-background-color: #333;
@@ -111,6 +112,18 @@ html[data-theme="dark"] {
111
112
  color: white;
112
113
  }
113
114
 
115
+ & table thead th.sortable:hover,
116
+ & table thead th.filtered,
117
+ & table thead th.menu-open {
118
+ color: var(--text-color);
119
+ }
120
+
121
+ & th.actions .actions-menu-trigger button:hover,
122
+ & th.actions .actions-menu-trigger button:focus-visible {
123
+ background-color: var(--hover-background-color);
124
+ color: var(--text-color);
125
+ }
126
+
114
127
  & footer span {
115
128
  background-color: #444;
116
129
  }
@@ -177,8 +190,10 @@ html[data-theme="dark"] {
177
190
  box-shadow: 0 0 .5em #121212;
178
191
  }
179
192
 
180
- & aside :is(ul, menu) li a:hover {
193
+ & aside :is(ul, menu) li a:hover,
194
+ & aside :is(ul, menu) li a:focus-visible {
181
195
  background-color: var(--hover-background-color);
196
+ color: var(--hover-text-color, #fff);
182
197
  }
183
198
 
184
199
  & dt {
@@ -0,0 +1,9 @@
1
+ # PicoCrank supplemental themes
2
+
3
+ Optional drop-in themes (Catppuccin Latte / Frappé, Dracula / Alucard, Gruvbox (Dark+Light), waffles) shipped alongside the default build unless disabled.
4
+
5
+ - **Build / dev assets:** included by default. Set `VITE_INCLUDE_SUPPLEMENTAL_THEMES=false` for a minimal build (`npm run build:minimal`).
6
+ - **Runtime:** opt in with `includeSupplementalThemes` on `ThemeSwitcher` or `useCustomTheme()` (default `false`). Client apps do not list or apply these themes unless explicitly enabled.
7
+ - **Catppuccin Latte / Frappé:** one theme folder (`catppuccin-latte-frappe`); header light/dark toggle switches between Latte (light) and Frappé (dark).
8
+ - **Gruvbox (Dark+Light):** one theme folder (`gruvbox-dark-light`); header light/dark toggle switches between Gruvbox Dark and Light.
9
+ - **Dracula / Alucard:** one theme folder (`dracula-alucard`); header light/dark toggle switches between Dracula (dark) and Alucard (light).
@@ -0,0 +1,276 @@
1
+ /*
2
+ * Catppuccin Latte / Frappé supplemental theme for PicoCrank / Femtocrank.
3
+ * Palette: https://catppuccin.com/palette/
4
+ *
5
+ * Light mode (header toggle) uses Latte; dark mode uses Frappé.
6
+ *
7
+ * Asset URLs must be absolute, e.g.:
8
+ * background-image: url('/supplemental-themes/catppuccin-latte-frappe/background.png');
9
+ */
10
+
11
+ html[data-theme="dark"] {
12
+ color-scheme: dark;
13
+
14
+ --ctp-rosewater: #f2d5cf;
15
+ --ctp-flamingo: #eebebe;
16
+ --ctp-pink: #f4b8e4;
17
+ --ctp-mauve: #ca9ee6;
18
+ --ctp-red: #e78284;
19
+ --ctp-maroon: #ea999c;
20
+ --ctp-peach: #ef9f76;
21
+ --ctp-yellow: #e5c890;
22
+ --ctp-green: #a6d189;
23
+ --ctp-teal: #81c8be;
24
+ --ctp-sky: #99d1db;
25
+ --ctp-sapphire: #85c1dc;
26
+ --ctp-blue: #8caaee;
27
+ --ctp-lavender: #babbf1;
28
+ --ctp-text: #c6d0f5;
29
+ --ctp-subtext1: #b5bfe2;
30
+ --ctp-subtext0: #a5adce;
31
+ --ctp-overlay2: #949cbb;
32
+ --ctp-overlay1: #838ba7;
33
+ --ctp-overlay0: #737994;
34
+ --ctp-surface2: #626880;
35
+ --ctp-surface1: #51576d;
36
+ --ctp-surface0: #414559;
37
+ --ctp-base: #303446;
38
+ --ctp-mantle: #292c3c;
39
+ --ctp-crust: #232634;
40
+ --ctp-shadow: rgba(35, 38, 52, 0.65);
41
+ --ctp-shadow-strong: rgba(35, 38, 52, 0.9);
42
+ --ctp-good-fg: var(--ctp-crust);
43
+
44
+ --text-color: var(--ctp-text);
45
+ --border-color: var(--ctp-surface1);
46
+ --standout-bg-color: var(--ctp-mantle);
47
+ --hover-background-color: var(--ctp-surface0);
48
+ --hover-text-color: var(--ctp-text);
49
+ --header-hover-background-color: var(--ctp-surface1);
50
+ --header-hover-text-color: var(--ctp-text);
51
+ --header-text-color: var(--ctp-text);
52
+ --disabled-background-color: var(--ctp-surface0);
53
+ --disabled-text-color: var(--ctp-overlay0);
54
+
55
+ --karma-good: var(--ctp-green);
56
+ --karma-bad: var(--ctp-red);
57
+ --karma-warning: var(--ctp-yellow);
58
+ --karma-severe: var(--ctp-maroon);
59
+ --karma-important: var(--ctp-peach);
60
+ --karma-note: var(--ctp-sapphire);
61
+ --karma-info: var(--ctp-surface1);
62
+ --karma-old: var(--ctp-overlay0);
63
+
64
+ --karma-good-fg: var(--ctp-green);
65
+ --karma-bad-fg: var(--ctp-red);
66
+ --karma-warning-fg: var(--ctp-yellow);
67
+ --karma-severe-fg: var(--ctp-maroon);
68
+ --karma-important-fg: var(--ctp-peach);
69
+ --karma-note-fg: var(--ctp-sapphire);
70
+ --karma-info-fg: var(--ctp-subtext0);
71
+ --karma-old-fg: var(--ctp-overlay1);
72
+ }
73
+
74
+ html[data-theme="light"] {
75
+ color-scheme: light;
76
+
77
+ --ctp-rosewater: #dc8a78;
78
+ --ctp-flamingo: #dd7878;
79
+ --ctp-pink: #ea76cb;
80
+ --ctp-mauve: #8839ef;
81
+ --ctp-red: #d20f39;
82
+ --ctp-maroon: #e64553;
83
+ --ctp-peach: #fe640b;
84
+ --ctp-yellow: #df8e1d;
85
+ --ctp-green: #40a02b;
86
+ --ctp-teal: #179299;
87
+ --ctp-sky: #04a5e5;
88
+ --ctp-sapphire: #209fb5;
89
+ --ctp-blue: #1e66f5;
90
+ --ctp-lavender: #7287fd;
91
+ --ctp-text: #4c4f69;
92
+ --ctp-subtext1: #5c5f77;
93
+ --ctp-subtext0: #6c6f85;
94
+ --ctp-overlay2: #7c7f93;
95
+ --ctp-overlay1: #8c8fa1;
96
+ --ctp-overlay0: #9ca0b0;
97
+ --ctp-surface2: #acb0be;
98
+ --ctp-surface1: #bcc0cc;
99
+ --ctp-surface0: #ccd0da;
100
+ --ctp-base: #eff1f5;
101
+ --ctp-mantle: #e6e9ef;
102
+ --ctp-crust: #dce0e8;
103
+ --ctp-shadow: rgba(76, 79, 105, 0.12);
104
+ --ctp-shadow-strong: rgba(76, 79, 105, 0.16);
105
+ --ctp-good-fg: var(--ctp-base);
106
+
107
+ --text-color: var(--ctp-text);
108
+ --border-color: var(--ctp-surface1);
109
+ --standout-bg-color: var(--ctp-mantle);
110
+ --hover-background-color: var(--ctp-surface0);
111
+ --hover-text-color: var(--ctp-text);
112
+ --header-hover-background-color: var(--ctp-surface0);
113
+ --header-hover-text-color: var(--ctp-text);
114
+ --header-text-color: var(--ctp-text);
115
+ --disabled-background-color: var(--ctp-surface0);
116
+ --disabled-text-color: var(--ctp-overlay0);
117
+
118
+ --karma-good: var(--ctp-green);
119
+ --karma-bad: var(--ctp-red);
120
+ --karma-warning: var(--ctp-yellow);
121
+ --karma-severe: var(--ctp-maroon);
122
+ --karma-important: var(--ctp-peach);
123
+ --karma-note: var(--ctp-sapphire);
124
+ --karma-info: var(--ctp-surface1);
125
+ --karma-old: var(--ctp-overlay0);
126
+
127
+ --karma-good-fg: var(--ctp-green);
128
+ --karma-bad-fg: var(--ctp-red);
129
+ --karma-warning-fg: var(--ctp-yellow);
130
+ --karma-severe-fg: var(--ctp-maroon);
131
+ --karma-important-fg: var(--ctp-peach);
132
+ --karma-note-fg: var(--ctp-sapphire);
133
+ --karma-info-fg: var(--ctp-subtext0);
134
+ --karma-old-fg: var(--ctp-overlay1);
135
+ }
136
+
137
+ body {
138
+ background-color: var(--ctp-base);
139
+ color: var(--ctp-text);
140
+ }
141
+
142
+ header {
143
+ background-color: var(--ctp-crust);
144
+ border-bottom: 1px solid var(--ctp-surface0);
145
+ box-shadow: none;
146
+ color: var(--ctp-text);
147
+ }
148
+
149
+ header a,
150
+ header a:visited,
151
+ header button,
152
+ header .button,
153
+ header h1,
154
+ header .topbar-link,
155
+ header .topbar-link:visited {
156
+ color: var(--header-text-color, var(--ctp-text));
157
+ }
158
+
159
+ #sidebar-toggler-button,
160
+ #sidebar-toggler-button.neutral,
161
+ header .theme-toggle,
162
+ header .theme-toggle.neutral,
163
+ header .search-trigger,
164
+ header .search-trigger.neutral,
165
+ header .user-info {
166
+ background-color: transparent;
167
+ border: 0;
168
+ color: var(--header-text-color, var(--ctp-text));
169
+ }
170
+
171
+ #sidebar-toggler-button:hover,
172
+ header .theme-toggle:hover,
173
+ header .search-trigger:hover,
174
+ header .user-info:hover {
175
+ background-color: var(--header-hover-background-color);
176
+ color: var(--header-hover-text-color);
177
+ }
178
+
179
+ aside,
180
+ #sidebar {
181
+ background-color: var(--ctp-mantle);
182
+ color: var(--ctp-text);
183
+ border-color: var(--ctp-surface0);
184
+ box-shadow: 0.15em 0 0.6em var(--ctp-shadow);
185
+ }
186
+
187
+ aside a,
188
+ aside a:visited {
189
+ color: var(--ctp-text);
190
+ }
191
+
192
+ aside a:hover,
193
+ aside a:focus-visible,
194
+ aside :is(ul, menu) li a:hover,
195
+ aside :is(ul, menu) li a:focus-visible,
196
+ aside .nav-section-header:hover,
197
+ aside .nav-section-header:focus-visible {
198
+ background-color: var(--ctp-surface0);
199
+ color: var(--ctp-text);
200
+ }
201
+
202
+ section {
203
+ background-color: var(--ctp-mantle);
204
+ color: var(--ctp-text);
205
+ box-shadow: 0 0 0.5em var(--ctp-shadow-strong);
206
+ border-color: var(--ctp-surface0);
207
+ }
208
+
209
+ section .section-header,
210
+ section .section-subheader {
211
+ background-color: var(--ctp-mantle);
212
+ border-bottom-color: var(--ctp-surface0);
213
+ color: var(--ctp-text);
214
+ }
215
+
216
+ button,
217
+ .button {
218
+ background-color: var(--ctp-surface0);
219
+ border: 1px solid var(--ctp-surface1);
220
+ color: var(--ctp-text);
221
+ }
222
+
223
+ button:hover,
224
+ .button:hover {
225
+ background-color: var(--ctp-surface1);
226
+ color: var(--ctp-text);
227
+ }
228
+
229
+ button.good,
230
+ .button.good {
231
+ background-color: var(--ctp-green);
232
+ border-color: var(--ctp-green);
233
+ color: var(--ctp-good-fg);
234
+ }
235
+
236
+ button.bad,
237
+ .button.bad {
238
+ background-color: var(--ctp-red);
239
+ border-color: var(--ctp-red);
240
+ color: var(--ctp-crust);
241
+ }
242
+
243
+ button.warning,
244
+ .button.warning {
245
+ background-color: var(--ctp-yellow);
246
+ border-color: var(--ctp-yellow);
247
+ color: var(--ctp-crust);
248
+ }
249
+
250
+ button.neutral,
251
+ .button.neutral {
252
+ background-color: var(--ctp-surface1);
253
+ border-color: var(--ctp-surface2);
254
+ color: var(--ctp-text);
255
+ }
256
+
257
+ input,
258
+ select,
259
+ textarea {
260
+ background-color: var(--ctp-surface0);
261
+ border: 1px solid var(--ctp-surface1);
262
+ color: var(--ctp-text);
263
+ }
264
+
265
+ a {
266
+ color: var(--ctp-blue);
267
+ }
268
+
269
+ footer {
270
+ color: var(--ctp-subtext0);
271
+ border-top-color: var(--ctp-surface0);
272
+ }
273
+
274
+ .subtle {
275
+ color: var(--ctp-subtext0);
276
+ }