picocrank 1.25.0 → 1.25.2

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.
@@ -116,7 +116,7 @@ defineExpose({
116
116
 
117
117
  .tabs-header {
118
118
  display: flex;
119
- border-bottom: 2px solid var(--border-color, #e1e5e9);
119
+ border-bottom: 2px solid var(--tab-border);
120
120
  gap: 0.25rem;
121
121
  overflow-x: auto;
122
122
  scrollbar-width: thin;
@@ -131,7 +131,7 @@ defineExpose({
131
131
  }
132
132
 
133
133
  .tabs-header::-webkit-scrollbar-thumb {
134
- background: var(--border-color, #ccc);
134
+ background: var(--tab-border);
135
135
  border-radius: 2px;
136
136
  }
137
137
 
@@ -146,25 +146,25 @@ defineExpose({
146
146
  border-radius: 0;
147
147
  cursor: pointer;
148
148
  font-size: 1em;
149
- color: var(--text-muted, #666);
149
+ color: var(--tab-fg);
150
150
  transition: all 0.2s ease;
151
151
  white-space: nowrap;
152
152
  position: relative;
153
153
  }
154
154
 
155
155
  .tab-button:hover {
156
- color: var(--text-color, #000);
157
- background-color: var(--hover-background-color, #f5f5f5);
156
+ color: var(--tab-hover-fg);
157
+ background-color: var(--tab-hover-bg);
158
158
  }
159
159
 
160
160
  .tab-button.active {
161
- color: var(--primary-color, #007bff);
162
- border-bottom-color: var(--primary-color, #007bff);
161
+ color: var(--tab-active-fg);
162
+ border-bottom-color: var(--tab-active-fg);
163
163
  font-weight: bold;
164
164
  }
165
165
 
166
166
  .tab-button:focus {
167
- outline: 2px solid var(--primary-color, #007bff);
167
+ outline: 2px solid var(--tab-focus-outline-color);
168
168
  outline-offset: -2px;
169
169
  border-radius: 0;
170
170
  }
@@ -196,27 +196,6 @@ defineExpose({
196
196
  }
197
197
  }
198
198
 
199
- /* Dark theme support */
200
- html[data-theme="dark"] {
201
- .tabs-header {
202
- border-bottom-color: var(--border-color, #444);
203
- }
204
-
205
- .tab-button {
206
- color: var(--text-muted, #999);
207
- }
208
-
209
- .tab-button:hover {
210
- color: var(--text-color, #fff);
211
- background-color: var(--hover-background-color, #333);
212
- }
213
-
214
- .tab-button.active {
215
- color: var(--primary-color, #60a5fa);
216
- border-bottom-color: var(--primary-color, #60a5fa);
217
- }
218
- }
219
-
220
199
  /* Responsive */
221
200
  @media (max-width: 768px) {
222
201
  .tabs-header {
@@ -1,7 +1,13 @@
1
1
  <template>
2
- <label class="theme-switcher">
2
+ <label :class="['theme-switcher', { 'theme-switcher--compact': compact }]">
3
3
  <span v-if="label" class="theme-switcher-label">{{ label }}</span>
4
- <select :value="themePreference" @change="onThemeChange">
4
+ <select
5
+ ref="selectRef"
6
+ :id="selectId || undefined"
7
+ :value="themePreference"
8
+ :aria-label="compact ? (label || 'Active theme') : undefined"
9
+ @change="onThemeChange"
10
+ >
5
11
  <option value="">{{ defaultOptionLabel }}</option>
6
12
  <option
7
13
  v-for="name in availableThemes"
@@ -12,7 +18,7 @@
12
18
  </option>
13
19
  </select>
14
20
  </label>
15
- <p v-if="showEmptyHint && availableThemes.length === 0" class="subtle theme-switcher-hint">
21
+ <p v-if="showEmptyHint && !compact && availableThemes.length === 0" class="subtle theme-switcher-hint">
16
22
  <slot name="empty">
17
23
  No drop-in themes found. Add <code>public/themes/&lt;name&gt;/theme.css</code>
18
24
  or set <code>include-supplemental-themes</code> to list bundled supplemental themes.
@@ -20,8 +26,12 @@
20
26
  </p>
21
27
  </template>
22
28
 
29
+ <script>
30
+ export const HEADER_THEME_SWITCHER_SELECT_ID = 'picocrank-header-theme-switcher'
31
+ </script>
32
+
23
33
  <script setup>
24
- import { onMounted, watch } from 'vue'
34
+ import { onMounted, ref, watch } from 'vue'
25
35
  import { useCustomTheme } from '../composables/useCustomTheme.js'
26
36
 
27
37
  const props = defineProps({
@@ -31,7 +41,7 @@ const props = defineProps({
31
41
  },
32
42
  defaultOptionLabel: {
33
43
  type: String,
34
- default: 'Default (Femtocrank)',
44
+ default: 'Default (Femtocrank only)',
35
45
  },
36
46
  includeSupplementalThemes: {
37
47
  type: Boolean,
@@ -53,6 +63,14 @@ const props = defineProps({
53
63
  type: Boolean,
54
64
  default: false,
55
65
  },
66
+ compact: {
67
+ type: Boolean,
68
+ default: false,
69
+ },
70
+ selectId: {
71
+ type: String,
72
+ default: undefined,
73
+ },
56
74
  })
57
75
 
58
76
  const emit = defineEmits(['change'])
@@ -66,6 +84,17 @@ const {
66
84
  configure,
67
85
  } = useCustomTheme()
68
86
 
87
+ const selectRef = ref(null)
88
+
89
+ function focusSelect() {
90
+ selectRef.value?.focus()
91
+ selectRef.value?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
92
+ }
93
+
94
+ defineExpose({
95
+ focus: focusSelect,
96
+ })
97
+
69
98
  function syncOptions() {
70
99
  configure({
71
100
  includeSupplementalThemes: props.includeSupplementalThemes,
@@ -113,6 +142,16 @@ onMounted(() => {
113
142
  min-width: 14rem;
114
143
  }
115
144
 
145
+ .theme-switcher--compact {
146
+ flex-wrap: nowrap;
147
+ gap: 0;
148
+ }
149
+
150
+ .theme-switcher--compact select {
151
+ min-width: 10rem;
152
+ max-width: 13.5rem;
153
+ }
154
+
116
155
  .theme-switcher-hint {
117
156
  margin: 0.75rem 0 0;
118
157
  }
@@ -97,7 +97,7 @@ function handleLinkClick(callback = null) {
97
97
  padding: 0.5em 0.75em;
98
98
  border-radius: 0.25em;
99
99
  text-decoration: none;
100
- color: var(--header-text-color, inherit);
100
+ color: var(--header-fg-color, var(--header-text-color, inherit));
101
101
  transition: background-color 0.15s ease;
102
102
  white-space: nowrap;
103
103
  border: 0;
@@ -124,7 +124,7 @@ function handleLinkClick(callback = null) {
124
124
 
125
125
  html[data-theme="dark"] {
126
126
  .topbar-link {
127
- color: var(--header-text-color, #f8f9fa);
127
+ color: var(--header-fg-color, var(--header-text-color, #f8f9fa));
128
128
  }
129
129
 
130
130
  .separator {
@@ -2,6 +2,10 @@
2
2
  <Navigation ref="navigation">
3
3
  <Header username = "Guest" @toggleSidebar="toggleSidebar" @logoClick="goToIndex" @userClick="goToUserDetails" title = "PicoCrank" :logoUrl="logoUrl" :sidebarEnabled="sidebarEnabled" :navigation="navigation" :topBarEnabled="topBarEnabled" :showBranding="brandingEnabled" :breadcrumbs="breadcrumbsEnabled" :themeToggleEnabled="themeToggleEnabled">
4
4
  <template #toolbar>
5
+ <ExampleHeaderThemeSwitcher
6
+ v-if="themeSwitcherEnabled"
7
+ :include-supplemental-themes="includeSupplementalThemes"
8
+ />
5
9
  <QuickSearch
6
10
  v-if="quickSearchEnabled"
7
11
  ref="quickSearchRef"
@@ -37,6 +41,7 @@
37
41
  import { Pin02Icon, UserIcon } from '@hugeicons/core-free-icons'
38
42
 
39
43
  import QuickSearch from './../components/QuickSearch.vue'
44
+ import ExampleHeaderThemeSwitcher from './ExampleHeaderThemeSwitcher.vue'
40
45
  import Navigation from './../components/Navigation.vue'
41
46
  import NotificationPopups from './../components/NotificationPopups.vue'
42
47
  import '../../styles.css'
@@ -54,6 +59,8 @@
54
59
  const quickSearchEnabled = ref(true);
55
60
  const dummySearchFetchEnabled = ref(false);
56
61
  const themeToggleEnabled = ref(true);
62
+ const themeSwitcherEnabled = ref(true);
63
+ const includeSupplementalThemes = ref(true);
57
64
  const topBarEnabled = ref(false);
58
65
  const breadcrumbsEnabled = ref(false);
59
66
 
@@ -93,6 +100,10 @@
93
100
  themeToggleEnabled.value = !themeToggleEnabled.value;
94
101
  }
95
102
 
103
+ function toggleThemeSwitcherEnabled() {
104
+ themeSwitcherEnabled.value = !themeSwitcherEnabled.value;
105
+ }
106
+
96
107
  function toggleTopBarEnabled() {
97
108
  topBarEnabled.value = !topBarEnabled.value;
98
109
  }
@@ -161,6 +172,8 @@
161
172
  provide('toggleDummySearchFetchEnabled', toggleDummySearchFetchEnabled);
162
173
  provide('themeToggleEnabled', themeToggleEnabled);
163
174
  provide('toggleThemeToggleEnabled', toggleThemeToggleEnabled);
175
+ provide('themeSwitcherEnabled', themeSwitcherEnabled);
176
+ provide('toggleThemeSwitcherEnabled', toggleThemeSwitcherEnabled);
164
177
  provide('topBarEnabled', topBarEnabled);
165
178
  provide('toggleTopBarEnabled', toggleTopBarEnabled);
166
179
  provide('breadcrumbsEnabled', breadcrumbsEnabled);
@@ -0,0 +1,78 @@
1
+ <template>
2
+ <ThemeSwitcher
3
+ v-if="enabled"
4
+ compact
5
+ label=""
6
+ :select-id="HEADER_THEME_SWITCHER_SELECT_ID"
7
+ default-option-label="Default (Femtocrank only)"
8
+ :include-supplemental-themes="includeSupplementalThemes"
9
+ />
10
+ </template>
11
+
12
+ <script setup>
13
+ import ThemeSwitcher, { HEADER_THEME_SWITCHER_SELECT_ID } from '../components/ThemeSwitcher.vue'
14
+
15
+ defineProps({
16
+ enabled: {
17
+ type: Boolean,
18
+ default: true,
19
+ },
20
+ includeSupplementalThemes: {
21
+ type: Boolean,
22
+ default: true,
23
+ },
24
+ })
25
+ </script>
26
+
27
+ <!-- Examples app only: match Header toolbar chrome (see Header search-trigger styles). -->
28
+ <style>
29
+ /*
30
+ * Header renders theme toggle before the toolbar slot; use flex order to swap
31
+ * the drop-in theme selector ahead of the light/dark button without changing Header.
32
+ */
33
+ header .header-actions .theme-switcher--compact {
34
+ order: 1;
35
+ align-self: stretch;
36
+ display: inline-flex;
37
+ align-items: center;
38
+ margin: 0;
39
+ }
40
+
41
+ header .header-actions .theme-toggle {
42
+ order: 2;
43
+ }
44
+
45
+ header .header-actions .quick-search {
46
+ order: 3;
47
+ }
48
+
49
+ header .header-actions .user-info {
50
+ order: 4;
51
+ }
52
+
53
+ header .header-actions .theme-switcher--compact select {
54
+ align-self: stretch;
55
+ height: 100%;
56
+ border: 0;
57
+ border-radius: 0;
58
+ background-color: transparent;
59
+ color: var(--header-fg-color, var(--header-text-color, #fff));
60
+ padding-top: 0;
61
+ padding-bottom: 0;
62
+ padding-left: 0.6em;
63
+ padding-right: 0.6em;
64
+ font-size: small;
65
+ font-family: inherit;
66
+ cursor: pointer;
67
+ }
68
+
69
+ header .header-actions .theme-switcher--compact select:hover {
70
+ background-color: var(--header-hover-background-color);
71
+ color: var(--header-hover-text-color);
72
+ }
73
+
74
+ header .header-actions .theme-switcher--compact select:focus-visible {
75
+ outline: 2px solid var(--header-hover-text-color, #fff);
76
+ outline-offset: -2px;
77
+ }
78
+ </style>
@@ -62,6 +62,17 @@
62
62
  />
63
63
  </FormField>
64
64
 
65
+ <FormField label="Theme switcher" fake>
66
+ <RadioGroup
67
+ name="ps-theme-switcher"
68
+ variant="boolean"
69
+ aria-label="Theme switcher"
70
+ :model-value="themeSwitcherEnabled"
71
+ :options="showHideOptions"
72
+ @update:model-value="setThemeSwitcherEnabled"
73
+ />
74
+ </FormField>
75
+
65
76
  <FormField label="Top bar" fake>
66
77
  <div>
67
78
  <RadioGroup
@@ -170,6 +181,8 @@ const dummySearchFetchEnabled = inject('dummySearchFetchEnabled')
170
181
  const toggleDummySearchFetchEnabled = inject('toggleDummySearchFetchEnabled')
171
182
  const themeToggleEnabled = inject('themeToggleEnabled')
172
183
  const toggleThemeToggleEnabled = inject('toggleThemeToggleEnabled')
184
+ const themeSwitcherEnabled = inject('themeSwitcherEnabled')
185
+ const toggleThemeSwitcherEnabled = inject('toggleThemeSwitcherEnabled')
173
186
  const topBarEnabled = inject('topBarEnabled')
174
187
  const toggleTopBarEnabled = inject('toggleTopBarEnabled')
175
188
  const breadcrumbsEnabled = inject('breadcrumbsEnabled')
@@ -200,6 +213,10 @@ function setThemeToggleEnabled(want) {
200
213
  setFlag(themeToggleEnabled, toggleThemeToggleEnabled, want)
201
214
  }
202
215
 
216
+ function setThemeSwitcherEnabled(want) {
217
+ setFlag(themeSwitcherEnabled, toggleThemeSwitcherEnabled, want)
218
+ }
219
+
203
220
  function setTopBarEnabled(want) {
204
221
  setFlag(topBarEnabled, toggleTopBarEnabled, want)
205
222
  }
@@ -8,24 +8,24 @@
8
8
  optionally <code>supplemental-themes/</code> (Catppuccin, Dracula / Alucard, Gruvbox, waffles). Drop a folder
9
9
  in and restart the dev server (or rebuild); PicoCrank discovers themes from
10
10
  <code>index.json</code> and applies them with <code>useCustomTheme()</code> or
11
- <code>ThemeSwitcher</code>. Supplemental themes are bundled by default but only
11
+ <code>ThemeSwitcher</code> in the examples app header toolbar. Supplemental themes are bundled by default but only
12
12
  listed when <code>include-supplemental-themes</code> is enabled.
13
13
  </p>
14
14
 
15
- <ThemeSwitcher
16
- class="theme-switcher-block"
17
- :include-supplemental-themes="true"
18
- @change="onThemeChange"
19
- />
20
-
21
- <p v-if="availableThemes.length === 0" class="subtle">
22
- No drop-in themes found. Add <code>public/themes/&lt;name&gt;/theme.css</code>
23
- or enable supplemental themes on <code>ThemeSwitcher</code>.
24
- </p>
25
- <p v-else class="subtle">
15
+ <p class="subtle">
16
+ Use the theme dropdown in the header (next to the light/dark toggle) to switch drop-in themes.
26
17
  Preference is stored in localStorage and applies across the whole examples site.
27
- Light/dark mode (header sun/moon) switches Femtocrank defaults, or the dual-variant supplemental themes when active.
18
+ Light/dark mode switches Femtocrank defaults, or the dual-variant supplemental themes when active.
28
19
  </p>
20
+
21
+ <button
22
+ type="button"
23
+ class="neutral open-theme-switcher-button"
24
+ :disabled="!themeSwitcherEnabled"
25
+ @click="openThemeSwitcher"
26
+ >
27
+ Open theme switcher
28
+ </button>
29
29
  </Section>
30
30
 
31
31
  <Section title="Buttons" subtitle="Default and karma variants">
@@ -106,28 +106,49 @@
106
106
  <td><code>--karma-*</code></td>
107
107
  <td>Status backgrounds and accents</td>
108
108
  </tr>
109
+ <tr>
110
+ <td><code>--link-color</code></td>
111
+ <td>Links, active tabs, sort/filter column headers</td>
112
+ </tr>
113
+ <tr>
114
+ <td><code>--muted-text-color</code></td>
115
+ <td>Secondary text and inactive tab labels</td>
116
+ </tr>
117
+ <tr>
118
+ <td><code>--header-fg-color</code></td>
119
+ <td>Header chrome text and icons</td>
120
+ </tr>
121
+ <tr>
122
+ <td><code>--input-bg-color</code> / <code>--input-fg-color</code></td>
123
+ <td>Form fields</td>
124
+ </tr>
109
125
  </tbody>
110
126
  </table>
111
127
  </Section>
112
128
  </template>
113
129
 
114
130
  <script setup>
131
+ import { inject } from 'vue'
115
132
  import Section from '../components/Section.vue'
116
- import ThemeSwitcher from '../components/ThemeSwitcher.vue'
117
133
  import FormLayout from '../components/FormLayout.vue'
118
134
  import FormField from '../components/FormField.vue'
119
- import { useCustomTheme } from '../composables/useCustomTheme.js'
135
+ import { HEADER_THEME_SWITCHER_SELECT_ID } from '../components/ThemeSwitcher.vue'
120
136
 
121
- const { availableThemes } = useCustomTheme()
137
+ const themeSwitcherEnabled = inject('themeSwitcherEnabled')
122
138
 
123
- function onThemeChange() {
124
- // ThemeSwitcher updates shared useCustomTheme state.
139
+ function openThemeSwitcher() {
140
+ const select = document.getElementById(HEADER_THEME_SWITCHER_SELECT_ID)
141
+ if (!(select instanceof HTMLSelectElement)) {
142
+ return
143
+ }
144
+ select.focus()
145
+ select.scrollIntoView({ block: 'nearest', inline: 'nearest' })
125
146
  }
126
147
  </script>
127
148
 
128
149
  <style scoped>
129
- .theme-switcher-block {
130
- margin: 1rem 0;
150
+ .open-theme-switcher-button {
151
+ margin-top: 0.75rem;
131
152
  }
132
153
 
133
154
  .preview-row {
@@ -47,24 +47,6 @@
47
47
  </Navigation>
48
48
  </Section>
49
49
 
50
- <Section
51
- title="Popular examples"
52
- subtitle="A few good places to begin"
53
- >
54
- <div class="grid-boxed welcome-popular-grid">
55
- <router-link
56
- v-for="example in popularExamples"
57
- :key="example.name"
58
- :to="{ name: example.name }"
59
- class="stat-display welcome-popular-card"
60
- >
61
- <span class="subtle">{{ example.category }}</span>
62
- <span class="stat">{{ example.label }}</span>
63
- <span class="welcome-popular-description">{{ example.description }}</span>
64
- </router-link>
65
- </div>
66
- </Section>
67
-
68
50
  <Section
69
51
  title="Try it"
70
52
  subtitle="Small interactions you can trigger right now"
@@ -109,33 +91,6 @@ import logoUrl from '/logo.png'
109
91
  const { show: showPopup } = useNotificationPopups()
110
92
  const welcomeNavigation = ref(null)
111
93
 
112
- const popularExamples = [
113
- {
114
- name: 'ThemePreview',
115
- category: 'Layouts',
116
- label: 'Theme Switcher',
117
- description: 'Switch drop-in themes live',
118
- },
119
- {
120
- name: 'TableExample',
121
- category: 'Data display',
122
- label: 'Table',
123
- description: 'Sorting, pagination, and column filters',
124
- },
125
- {
126
- name: 'CalendarExample',
127
- category: 'Data display',
128
- label: 'Calendar',
129
- description: 'Events, drag-and-drop, and date ranges',
130
- },
131
- {
132
- name: 'ButtonsExample',
133
- category: 'Forms & input',
134
- label: 'Buttons',
135
- description: 'Variants, toolbars, and form actions',
136
- },
137
- ]
138
-
139
94
  function showWelcomeToast() {
140
95
  showPopup({
141
96
  label: 'WELCOME',
@@ -212,31 +167,4 @@ onMounted(() => {
212
167
  gap: 0.5rem;
213
168
  margin-bottom: 0.75rem;
214
169
  }
215
-
216
- .welcome-popular-grid {
217
- grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
218
- }
219
-
220
- .welcome-popular-card {
221
- display: flex;
222
- flex-direction: column;
223
- gap: 0.35rem;
224
- text-decoration: none;
225
- color: inherit;
226
- transition: transform 0.15s ease, box-shadow 0.15s ease;
227
- }
228
-
229
- .welcome-popular-card:hover {
230
- transform: translateY(-2px);
231
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
232
- }
233
-
234
- .welcome-popular-description {
235
- font-size: 0.85rem;
236
- color: #666;
237
- }
238
-
239
- html[data-theme="dark"] .welcome-popular-description {
240
- color: #bbb;
241
- }
242
170
  </style>