picocrank 1.24.0 → 1.25.1

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 (41) 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 +34 -2
  6. package/public/themes/index.json +1 -5
  7. package/styles/dark-theme.css +42 -186
  8. package/styles/picocrank-extensions.css +15 -0
  9. package/styles.css +1 -0
  10. package/supplemental-themes/README.md +9 -0
  11. package/supplemental-themes/catppuccin-latte-frappe/theme.css +211 -0
  12. package/supplemental-themes/dracula-alucard/theme.css +193 -0
  13. package/supplemental-themes/gruvbox-dark-light/theme.css +199 -0
  14. package/supplemental-themes/index.json +17 -0
  15. package/{public/themes → supplemental-themes}/waffles/theme.css +69 -46
  16. package/vite.config.js +91 -0
  17. package/vue/components/Header.vue +3 -3
  18. package/vue/components/Login.vue +1 -3
  19. package/vue/components/Section.vue +45 -14
  20. package/vue/components/Sidebar.vue +6 -11
  21. package/vue/components/SidebarNavLink.vue +6 -9
  22. package/vue/components/Table.vue +1115 -75
  23. package/vue/components/TableColumnFilterPopover.vue +263 -44
  24. package/vue/components/TableColumnOptionsPopover.vue +1116 -0
  25. package/vue/components/Tabs.vue +4 -25
  26. package/vue/components/ThemeSwitcher.vue +158 -0
  27. package/vue/components/TopBar.vue +2 -2
  28. package/vue/composables/tableColumnPresets.js +296 -0
  29. package/vue/composables/tableFilterQuery.js +202 -0
  30. package/vue/composables/tableFilters.js +167 -3
  31. package/vue/composables/useCustomTheme.js +167 -24
  32. package/vue/data/examplePeople.js +2 -6
  33. package/vue/examples/App.vue +14 -1
  34. package/vue/examples/ExampleHeaderThemeSwitcher.vue +78 -0
  35. package/vue/examples/LoginExample.vue +1 -3
  36. package/vue/examples/PageStructure.vue +17 -0
  37. package/vue/examples/TableExample.vue +329 -11
  38. package/vue/examples/TableRemoteExample.vue +112 -4
  39. package/vue/examples/ThemePreview.vue +45 -38
  40. package/public/themes/catppuccin/theme.css +0 -195
  41. 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.24.0",
3
+ "version": "1.25.1",
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.1",
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,36 @@ 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.
47
+
48
+ ## PicoCrank component tokens
49
+
50
+ Sortable/filterable `Table` headers and `Tabs` active labels use PicoCrank aliases that point at Femtocrank semantic variables. Override the underlying Femtocrank tokens in `theme.css` rather than the aliases when possible:
51
+
52
+ | PicoCrank alias | Femtocrank source |
53
+ | --- | --- |
54
+ | `--table-header-fg` | `--text-color` |
55
+ | `--table-header-active-fg` | `--link-color` |
56
+ | `--table-header-active-bg` | `--hover-background-color` |
57
+ | `--tab-fg` | `--muted-text-color` |
58
+ | `--tab-active-fg` | `--link-color` |
59
+ | `--tab-focus-outline-color` | `--tab-active-fg` |
60
+
61
+ Defaults are defined in `styles/picocrank-extensions.css`. Built-in dark mode and supplemental themes set Femtocrank variables (`--link-color`, `--header-fg-color`, `--input-bg-color`, etc.) so tables, tabs, forms, and chrome pick up the palette automatically.
@@ -1,7 +1,3 @@
1
1
  {
2
- "themes": [
3
- "catppuccin",
4
- "dracula",
5
- "waffles"
6
- ]
2
+ "themes": []
7
3
  }
@@ -1,204 +1,60 @@
1
+ /* Built-in dark mode — variable overrides only (mirrors femtocrank/dark.css). */
1
2
  @layer components {
2
3
  html[data-theme="dark"] {
3
- color-scheme: dark;
4
+ color-scheme: dark;
4
5
 
5
- --border-color: #595959;
6
- --hover-background-color: #1d345c;
7
- --hover-text-color: #fff;
8
- --header-hover-background-color: #1d345c;
9
- --standout-bg-color: #1f1f1f;
10
- --disabled-background-color: #333;
11
- --disabled-text-color: #999;
6
+ --body-bg-color: #262626;
7
+ --surface-bg-color: #343434;
8
+ --standout-bg-color: #1f1f1f;
9
+ --section-header-bg-color: var(--surface-bg-color);
12
10
 
13
- & header {
14
- background-color: black;
15
- box-shadow: 0px 0px 1em #1c1c1c;
16
- border-bottom: 1px solid #222;
17
- }
11
+ --header-bg-color: #000;
12
+ --header-border-color: #222;
13
+ --header-shadow: 0px 0px 1em #1c1c1c;
14
+ --header-hover-background-color: #1d345c;
18
15
 
19
- & body {
20
- background-color: #262626;
21
- color: #ddd;
22
- }
16
+ --sidebar-bg-color: #000;
17
+ --sidebar-link-color: var(--link-color);
18
+ --sidebar-hover-bg-color: var(--hover-background-color);
23
19
 
24
- & section {
25
- background-color: #343434;
26
- color: #ddd;
27
- box-shadow: 0 0 .5em #121212;
28
- }
20
+ --footer-chip-bg-color: #444;
29
21
 
30
- & section .section-header {
31
- background-color: #343434;
32
- border-bottom-color: var(--border-color);
33
- }
22
+ --text-color: #ddd;
23
+ --link-color: lightblue;
24
+ --annotation-key-color: #aaa;
25
+ --focus-outline-color: #fff;
34
26
 
35
- & section .section-subheader {
36
- border-bottom-color: var(--border-color);
37
- }
27
+ --border-color: #595959;
28
+ --shadow-color: #121212;
38
29
 
39
- & button,
40
- & .button {
41
- color: #ddd;
42
- border: 1px solid var(--border-color);
43
- }
30
+ --hover-background-color: #1d345c;
31
+ --hover-text-color: var(--on-accent-fg);
44
32
 
45
- & button:hover,
46
- & .button:hover,
47
- & .button:visited {
48
- background-color: var(--hover-background-color);
49
- color: white;
50
- }
33
+ --disabled-background-color: #333;
34
+ --disabled-text-color: #999;
51
35
 
52
- & footer a,
53
- & footer a:visited {
54
- color: lightblue;
55
- }
36
+ --input-bg-color: #4e4e4e;
37
+ --input-fg-color: var(--text-color);
38
+ --control-option-bg-color: #555;
39
+ --control-hover-bg-color: var(--hover-background-color);
56
40
 
57
- & header ul[role="menubar"] a,
58
- & header ul[role="menubar"] a:visited {
59
- background-color: black;
60
- }
41
+ --menu-item-hover-background-color: var(--hover-background-color);
42
+ --menu-dropdown-background-color: var(--surface-bg-color);
43
+ --menu-dropdown-shadow: 0 0 .5em var(--shadow-color);
44
+ --menu-dropdown-link-hover-background-color: var(--hover-background-color);
45
+ --header-menu-dropdown-link-hover-background-color: var(--header-hover-background-color);
61
46
 
62
- & header ul[role="menubar"] a:hover {
63
- background-color: var(--hover-background-color);
64
- }
47
+ --table-sticky-bg: var(--surface-bg-color);
48
+ --table-row-hover-bg: var(--hover-background-color);
49
+ --table-row-hover-fg: var(--on-accent-fg);
50
+ --table-sticky-hover-bg: var(--table-row-hover-bg);
51
+ --table-uneditable-bg: #555;
52
+ --table-sticky-uneditable-bg: var(--table-uneditable-bg);
65
53
 
66
- & header div > ul > li > a {
67
- background-color: black;
68
- color: white;
69
- }
70
-
71
- & ul[role="menubar"] div ul a:hover {
72
- background-color: var(--hover-background-color);
73
- color: white;
74
- }
75
-
76
- & ul[role="menubar"] li:hover {
77
- background-color: var(--hover-background-color);
78
- }
79
-
80
- & label:has(input[type="checkbox"]):hover,
81
- & label:has(input[type="radio"]):hover {
82
- background-color: var(--hover-background-color);
83
- color: white;
84
- }
85
-
86
- & label:has(input:checked) {
87
- background-color: #488448;
88
- color: white;
89
- }
90
-
91
- & label:has(input:checked):hover {
92
- background-color: #3a6f3a;
93
- color: white;
94
- }
95
-
96
- & a,
97
- & a:visited,
98
- & aside a,
99
- & aside a:visited {
100
- color: lightblue;
101
- }
102
-
103
- & table,
104
- & th,
105
- & td {
106
- border-color: var(--border-color) !important;
107
- }
108
-
109
- & table.row-hover tr:hover td {
110
- background-color: var(--hover-background-color);
111
- color: white;
112
- }
113
-
114
- & footer span {
115
- background-color: #444;
116
- }
117
-
118
- & input[type="text"],
119
- & input[type="email"],
120
- & input[type="password"],
121
- & input[type="url"],
122
- & input[type="number"],
123
- & input[type="color"],
124
- & input[type="datetime-local"] {
125
- background-color: #4e4e4e;
126
- color: #ddd;
127
- border-color: var(--border-color);
128
- }
129
-
130
- & select {
131
- background-color: #4e4e4e;
132
- color: #ddd;
133
- border-color: var(--border-color);
134
- }
135
-
136
- & textarea {
137
- background-color: #4e4e4e;
138
- color: #ddd;
139
- border-color: var(--border-color);
140
- }
141
-
142
- & .grid-boxed .stat-display {
143
- background-color: var(--standout-bg-color);
144
- border-color: var(--border-color) !important;
145
- }
146
-
147
- & pre {
148
- background-color: var(--standout-bg-color);
149
- color: #ddd;
150
- border: 1px solid var(--border-color);
151
- }
152
-
153
- & .radio-group {
154
- border-color: var(--border-color);
155
- }
156
-
157
- & .radio-group label {
158
- background-color: #555;
159
- }
160
-
161
- & .radio-group label:hover,
162
- & .radio-group label:has(input:focus-visible) {
163
- background-color: var(--hover-background-color);
164
- color: white;
165
- }
166
-
167
- & .multiple-checklist {
168
- border-color: var(--border-color);
169
- }
170
-
171
- & .multiple-checklist label {
172
- background-color: #555;
173
- }
174
-
175
- & aside {
176
- background-color: #000;
177
- box-shadow: 0 0 .5em #121212;
178
- }
179
-
180
- & aside :is(ul, menu) li a:hover {
181
- background-color: var(--hover-background-color);
182
- }
183
-
184
- & dt {
185
- border-color: var(--border-color);
186
- }
187
-
188
- & .annotation,
189
- & .tag,
190
- & .inline-notification {
191
- background-color: var(--standout-bg-color);
192
- color: #ddd;
193
- border-color: var(--border-color);
194
- }
195
-
196
- & .annotation-val {
197
- color: #ddd;
198
- }
199
- }
54
+ --progress-track-bg-color: var(--table-uneditable-bg);
200
55
  }
201
56
 
202
57
  html[data-theme="light"] {
203
- color-scheme: light;
58
+ color-scheme: light;
59
+ }
204
60
  }
@@ -0,0 +1,15 @@
1
+ /* PicoCrank component tokens — aliases to Femtocrank semantic variables. */
2
+ @layer components {
3
+ :root {
4
+ --table-header-fg: var(--text-color);
5
+ --table-header-active-fg: var(--link-color);
6
+ --table-header-active-bg: var(--hover-background-color);
7
+ --tab-fg: var(--muted-text-color);
8
+ --tab-active-fg: var(--link-color);
9
+ --tab-focus-outline-color: var(--tab-active-fg);
10
+ }
11
+
12
+ table thead th {
13
+ color: var(--table-header-fg);
14
+ }
15
+ }
package/styles.css CHANGED
@@ -2,6 +2,7 @@
2
2
  @layer components, karma, theme;
3
3
 
4
4
  @import 'femtocrank/style.css' layer(components);
5
+ @import './styles/picocrank-extensions.css';
5
6
  @import './styles/dark-theme.css';
6
7
 
7
8
  /* Sidebar uses <menu> for nav items; mirror Femtocrank's aside ul rules. */
@@ -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).