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/vite.config.js CHANGED
@@ -4,9 +4,13 @@ import Components from 'unplugin-vue-components/vite'
4
4
  import fs from 'node:fs'
5
5
  import path from 'node:path'
6
6
  import { fileURLToPath } from 'node:url'
7
+ import { createReadStream, existsSync } from 'node:fs'
8
+ import { stat } from 'node:fs/promises'
7
9
 
8
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
9
11
  const themesDir = path.join(__dirname, 'public', 'themes')
12
+ const supplementalThemesDir = path.join(__dirname, 'supplemental-themes')
13
+ const includeSupplementalThemes = process.env.VITE_INCLUDE_SUPPLEMENTAL_THEMES !== 'false'
10
14
 
11
15
  function scanThemeNames() {
12
16
  if (!fs.existsSync(themesDir)) {
@@ -58,6 +62,92 @@ function themesIndexPlugin() {
58
62
  }
59
63
  }
60
64
 
65
+ function contentTypeFor(filePath) {
66
+ if (filePath.endsWith('.css')) {
67
+ return 'text/css; charset=utf-8'
68
+ }
69
+ if (filePath.endsWith('.json')) {
70
+ return 'application/json; charset=utf-8'
71
+ }
72
+ if (filePath.endsWith('.png')) {
73
+ return 'image/png'
74
+ }
75
+ if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) {
76
+ return 'image/jpeg'
77
+ }
78
+ if (filePath.endsWith('.webp')) {
79
+ return 'image/webp'
80
+ }
81
+ if (filePath.endsWith('.svg')) {
82
+ return 'image/svg+xml'
83
+ }
84
+ return 'application/octet-stream'
85
+ }
86
+
87
+ function copyDirectoryRecursive(sourceDir, targetDir) {
88
+ fs.mkdirSync(targetDir, { recursive: true })
89
+
90
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
91
+ const sourcePath = path.join(sourceDir, entry.name)
92
+ const targetPath = path.join(targetDir, entry.name)
93
+
94
+ if (entry.isDirectory()) {
95
+ copyDirectoryRecursive(sourcePath, targetPath)
96
+ continue
97
+ }
98
+
99
+ fs.copyFileSync(sourcePath, targetPath)
100
+ }
101
+ }
102
+
103
+ function supplementalThemesPlugin() {
104
+ return {
105
+ name: 'picocrank-supplemental-themes',
106
+ configureServer(server) {
107
+ if (!includeSupplementalThemes || !fs.existsSync(supplementalThemesDir)) {
108
+ return
109
+ }
110
+
111
+ server.middlewares.use('/supplemental-themes', (req, res, next) => {
112
+ const requestPath = decodeURIComponent((req.url || '/').split('?')[0])
113
+ const relativePath = requestPath.replace(/^\/+/, '')
114
+ const filePath = path.resolve(supplementalThemesDir, relativePath)
115
+
116
+ if (!filePath.startsWith(path.resolve(supplementalThemesDir))) {
117
+ res.statusCode = 403
118
+ res.end('Forbidden')
119
+ return
120
+ }
121
+
122
+ if (!existsSync(filePath)) {
123
+ next()
124
+ return
125
+ }
126
+
127
+ stat(filePath).then((fileStat) => {
128
+ if (!fileStat.isFile()) {
129
+ next()
130
+ return
131
+ }
132
+
133
+ res.setHeader('Content-Type', contentTypeFor(filePath))
134
+ createReadStream(filePath).pipe(res)
135
+ }).catch(() => {
136
+ next()
137
+ })
138
+ })
139
+ },
140
+ closeBundle() {
141
+ if (!includeSupplementalThemes || !fs.existsSync(supplementalThemesDir)) {
142
+ return
143
+ }
144
+
145
+ const outDir = path.resolve(__dirname, 'dist', 'supplemental-themes')
146
+ copyDirectoryRecursive(supplementalThemesDir, outDir)
147
+ },
148
+ }
149
+ }
150
+
61
151
  export default defineConfig({
62
152
  base: '/',
63
153
  build: {
@@ -93,6 +183,7 @@ export default defineConfig({
93
183
  },
94
184
  plugins: [
95
185
  themesIndexPlugin(),
186
+ supplementalThemesPlugin(),
96
187
  Components({
97
188
  dirs: ['./vue/components/'],
98
189
  extensions: ['vue'],
@@ -1,7 +1,35 @@
1
1
  <template>
2
2
  <label v-if="!fake" :for="controlId" :class="{ disabled }">{{ label }}</label>
3
3
  <span v-else class="fake-label" :class="{ disabled }">{{ label }}</span>
4
- <slot />
4
+ <div v-if="hasFieldMeta" class="form-field-control">
5
+ <template v-if="descriptionAbove">
6
+ <div v-if="description || docsUrl" class="form-field-meta">
7
+ <p v-if="description" class="subtle form-field-description">{{ description }}</p>
8
+ <a
9
+ v-if="docsUrl"
10
+ class="form-field-docs-link"
11
+ :href="docsUrl"
12
+ target="_blank"
13
+ rel="noopener noreferrer"
14
+ >{{ docsLinkTitle }}</a>
15
+ </div>
16
+ <slot />
17
+ </template>
18
+ <template v-else>
19
+ <slot />
20
+ <div v-if="description || docsUrl" class="form-field-meta">
21
+ <p v-if="description" class="subtle form-field-description">{{ description }}</p>
22
+ <a
23
+ v-if="docsUrl"
24
+ class="form-field-docs-link"
25
+ :href="docsUrl"
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ >{{ docsLinkTitle }}</a>
29
+ </div>
30
+ </template>
31
+ </div>
32
+ <slot v-else />
5
33
  </template>
6
34
 
7
35
  <script setup>
@@ -12,6 +40,22 @@ const props = defineProps({
12
40
  type: String,
13
41
  required: true,
14
42
  },
43
+ description: {
44
+ type: String,
45
+ default: '',
46
+ },
47
+ descriptionAbove: {
48
+ type: Boolean,
49
+ default: false,
50
+ },
51
+ docsUrl: {
52
+ type: String,
53
+ default: '',
54
+ },
55
+ docsUrlTitle: {
56
+ type: String,
57
+ default: '',
58
+ },
15
59
  for: {
16
60
  type: String,
17
61
  default: '',
@@ -31,6 +75,8 @@ const props = defineProps({
31
75
  })
32
76
 
33
77
  const controlId = computed(() => props.htmlFor || props.for)
78
+ const hasFieldMeta = computed(() => Boolean(props.description || props.docsUrl))
79
+ const docsLinkTitle = computed(() => props.docsUrlTitle || props.docsUrl)
34
80
  </script>
35
81
 
36
82
  <style scoped>
@@ -39,4 +85,26 @@ label.disabled,
39
85
  color: var(--disabled-text-color);
40
86
  cursor: not-allowed;
41
87
  }
88
+
89
+ .form-field-control {
90
+ display: flex;
91
+ flex-direction: column;
92
+ gap: 0.5rem;
93
+ width: 100%;
94
+ }
95
+
96
+ .form-field-meta {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: 0.35rem;
100
+ }
101
+
102
+ .form-field-description {
103
+ margin: 0;
104
+ }
105
+
106
+ .form-field-docs-link {
107
+ font-size: 0.9em;
108
+ align-self: flex-start;
109
+ }
42
110
  </style>
@@ -205,7 +205,7 @@
205
205
  .header-actions :deep(button) {
206
206
  border: 0;
207
207
  border-radius: 0;
208
- color: #fff;
208
+ color: var(--header-text-color, #fff);
209
209
  background-color: transparent;
210
210
  align-self: stretch;
211
211
  display: inline-flex;
@@ -227,7 +227,7 @@
227
227
  #sidebar-toggler-button {
228
228
  border: 0;
229
229
  border-radius: 0;
230
- color: #fff;
230
+ color: var(--header-text-color, #fff);
231
231
  background-color: transparent;
232
232
  align-self: stretch;
233
233
  display: inline-flex;
@@ -260,7 +260,7 @@ header .search-trigger,
260
260
  header .search-trigger.neutral {
261
261
  border: 0;
262
262
  border-radius: 0;
263
- color: #fff;
263
+ color: var(--header-text-color, #fff);
264
264
  background-color: transparent;
265
265
  align-self: stretch;
266
266
  height: 100%;
@@ -7,6 +7,7 @@
7
7
  <form @submit.prevent="handleLocalLogin" class="local-login-form">
8
8
  <input
9
9
  id="username"
10
+ type="text"
10
11
  v-model="localLogin.username"
11
12
  required
12
13
  autocomplete="username"
@@ -240,11 +241,8 @@ defineExpose({
240
241
 
241
242
  .local-login-form input {
242
243
  padding: 0.75rem;
243
- border: 1px solid var(--border-color, #ccc);
244
244
  border-radius: 4px;
245
245
  font-size: 1em;
246
- background-color: var(--input-bg, #fff);
247
- color: var(--text-color, #000);
248
246
  width: 100%;
249
247
  box-sizing: border-box;
250
248
  }
@@ -1,27 +1,41 @@
1
1
  <template>
2
- <section :class = "'with-header-and-content ' + classes" :id = "id">
3
- <div class = "section-header flex-row">
4
- <div class = "fg1">
5
- <h2 :title = "subtitle">
6
- <slot name="title">{{ title }}</slot>
2
+ <section :class="'with-header-and-content ' + classes" :id="id">
3
+ <div class="section-header flex-row">
4
+ <div class="fg1">
5
+ <h2 :title="subtitle">
6
+ <span v-if="icon" class="section-title-with-icon">
7
+ <HugeiconsIcon
8
+ :icon="icon"
9
+ class="section-title-icon"
10
+ width="1.1em"
11
+ height="1.1em"
12
+ :strokeWidth="2"
13
+ aria-hidden="true"
14
+ />
15
+ <span class="section-title-text">
16
+ <slot name="title">{{ title }}</slot>
17
+ </span>
18
+ </span>
19
+ <slot v-else name="title">{{ title }}</slot>
7
20
  </h2>
8
21
  </div>
9
- <div role = "toolbar">
22
+ <div role="toolbar">
10
23
  <slot name="toolbar" />
11
24
  </div>
12
25
  </div>
13
- <div :class = "padding ? 'section-content padding' : 'section-content'">
26
+ <div :class="padding ? 'section-content padding' : 'section-content'">
14
27
  <slot />
15
28
  </div>
16
29
  </section>
17
30
  </template>
18
31
 
19
32
  <script setup>
33
+ import { HugeiconsIcon } from '@hugeicons/vue'
20
34
 
21
- const props = defineProps({
35
+ defineProps({
22
36
  title: {
23
37
  type: String,
24
- default: "Untitled Section",
38
+ default: 'Untitled Section',
25
39
  },
26
40
  padding: {
27
41
  type: Boolean,
@@ -29,15 +43,32 @@ const props = defineProps({
29
43
  },
30
44
  subtitle: {
31
45
  type: String,
32
- default: "",
46
+ default: '',
33
47
  },
34
48
  id: {
35
49
  type: String,
36
- default: "",
50
+ default: '',
37
51
  },
38
52
  classes: {
39
53
  type: String,
40
- default: "",
54
+ default: '',
41
55
  },
42
- });
43
- </script>
56
+ icon: {
57
+ type: [Object, Array, Function],
58
+ default: null,
59
+ },
60
+ })
61
+ </script>
62
+
63
+ <style scoped>
64
+ .section-title-with-icon {
65
+ display: inline-flex;
66
+ align-items: center;
67
+ gap: 0.45em;
68
+ vertical-align: middle;
69
+ }
70
+
71
+ .section-title-icon {
72
+ flex: 0 0 auto;
73
+ }
74
+ </style>
@@ -420,7 +420,8 @@ button {
420
420
  cursor: pointer;
421
421
  }
422
422
 
423
- .nav-section-header:hover {
423
+ .nav-section-header:hover,
424
+ .nav-section-header:focus-visible {
424
425
  background-color: var(--hover-background-color);
425
426
  color: var(--hover-text-color);
426
427
  }
@@ -154,6 +154,12 @@ html[data-theme="dark"] {
154
154
  color: #f8f9fa;
155
155
  }
156
156
 
157
+ a:hover,
158
+ a:focus-visible {
159
+ background-color: var(--hover-background-color);
160
+ color: var(--hover-text-color, #fff);
161
+ }
162
+
157
163
  .separator {
158
164
  background-color: #444;
159
165
  }