picocrank 1.31.0 → 1.31.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.31.0",
3
+ "version": "1.31.1",
4
4
  "type": "module",
5
5
  "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
6
6
  "main": "vite.config.js",
@@ -91,7 +91,13 @@
91
91
  --danger-zone-warning-fg: var(--muted-text-color);
92
92
  --danger-zone-focus-outline: var(--karma-bad-border);
93
93
 
94
- --picocrank-fixed-header-offset: 4em;
94
+ /* Matches Femtocrank header min-height (logo 2em + .5em padding). */
95
+ --picocrank-fixed-header-offset: 3em;
96
+ }
97
+
98
+ /* No fixed header → no sidebar/main offset. */
99
+ html:not(:has(header.fixed)) {
100
+ --picocrank-fixed-header-offset: 0px;
95
101
  }
96
102
 
97
103
  table thead th {
@@ -172,9 +178,23 @@ select.field-invalid:focus-visible {
172
178
  }
173
179
 
174
180
  /*
175
- * Fixed header layout: pin the app header and offset sidebar + main content.
176
- * Apply .layout-fixed-header on a wrapper whose direct children are header and #layout.
181
+ * Fixed header layout: pin the app header and offset main content.
182
+ * Prefer Header's `fixed` prop (adds Femtocrank `.fixed` on <header>).
183
+ * Sidebar top/height use --picocrank-fixed-header-offset in Sidebar.vue
184
+ * (unlayered) so they are not overridden by stuck-mode defaults.
185
+ * Alternatively wrap with .layout-fixed-header when header and #layout are direct children.
177
186
  */
187
+ header.fixed {
188
+ top: 0;
189
+ left: 0;
190
+ z-index: 5;
191
+ }
192
+
193
+ header.fixed ~ aside,
194
+ header.fixed ~ #layout aside {
195
+ z-index: 3;
196
+ }
197
+
178
198
  .layout-fixed-header > header {
179
199
  position: fixed;
180
200
  top: 0;
@@ -183,15 +203,20 @@ select.field-invalid:focus-visible {
183
203
  z-index: 5;
184
204
  }
185
205
 
206
+ .layout-fixed-header > aside,
186
207
  .layout-fixed-header > #layout aside {
187
208
  z-index: 3;
188
- padding-top: var(--picocrank-fixed-header-offset);
209
+ }
189
210
  }
190
211
 
191
- .layout-fixed-header > #layout main {
212
+ /*
213
+ * Unlayered so this wins over @layer components (same issue as Sidebar offsets).
214
+ * Pads #content below a fixed header — clears the chrome and leaves a readable gap.
215
+ */
216
+ header.fixed ~ #layout > #content,
217
+ .layout-fixed-header > #layout > #content {
192
218
  padding-top: var(--picocrank-fixed-header-offset);
193
219
  }
194
- }
195
220
 
196
221
  /*
197
222
  * Responsive column priorities: classes col-priority-1 … col-priority-5 on th/td only
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <header>
2
+ <header :class="{ fixed }">
3
3
  <div
4
4
  :id = "showSidebarChrome ? 'sidebar-button' : null"
5
5
  :class="['image-and-title', 'flex-row', { 'disabled-branding': !showBranding, 'logo-home-link': !showSidebarChrome && showBranding }]"
@@ -146,6 +146,10 @@
146
146
  type: Boolean,
147
147
  default: false,
148
148
  },
149
+ fixed: {
150
+ type: Boolean,
151
+ default: true,
152
+ },
149
153
  navigation: {
150
154
  type: Object,
151
155
  default: null,
@@ -401,6 +401,34 @@ defineExpose({
401
401
  </script>
402
402
 
403
403
  <style scoped>
404
+ .sidebar {
405
+ display: flex;
406
+ flex-direction: column;
407
+ box-sizing: border-box;
408
+ overflow-x: hidden;
409
+ overflow-y: auto;
410
+ overscroll-behavior: contain;
411
+ /*
412
+ * Sit fully below a fixed Header (Navigation, pin, and scrollbar).
413
+ * Offset is 0 when no header.fixed is present (see picocrank-extensions).
414
+ */
415
+ top: var(--picocrank-fixed-header-offset);
416
+ height: calc(100vh - var(--picocrank-fixed-header-offset));
417
+ max-height: calc(100vh - var(--picocrank-fixed-header-offset));
418
+ }
419
+
420
+ /*
421
+ * Stuck mode: same viewport band as overlay mode; scroll the whole contents
422
+ * instead of growing with content (Femtocrank defaults to height: auto).
423
+ */
424
+ .sidebar.stuck {
425
+ align-self: flex-start;
426
+ position: sticky;
427
+ top: var(--picocrank-fixed-header-offset);
428
+ height: calc(100vh - var(--picocrank-fixed-header-offset));
429
+ max-height: calc(100vh - var(--picocrank-fixed-header-offset));
430
+ }
431
+
404
432
  h2 {
405
433
  padding: .75em;
406
434
  }
@@ -0,0 +1,211 @@
1
+ <template>
2
+ <component
3
+ :is="rootComponent"
4
+ v-bind="rootBindings"
5
+ :class="rootClasses"
6
+ >
7
+ <div
8
+ v-if="karma"
9
+ class="status-card-bar"
10
+ :class="{ 'status-card-bar--labeled': Boolean(barLabel) }"
11
+ >
12
+ <span
13
+ v-if="barLabel"
14
+ class="status-card-bar-label"
15
+ >{{ barLabel }}</span>
16
+ </div>
17
+ <div class="status-card-body">
18
+ <slot />
19
+ </div>
20
+ </component>
21
+ </template>
22
+
23
+ <script setup>
24
+ import { computed } from 'vue'
25
+ import { RouterLink } from 'vue-router'
26
+
27
+ const props = defineProps({
28
+ karma: {
29
+ type: String,
30
+ default: '',
31
+ validator: (value) => (
32
+ value === ''
33
+ || ['info', 'note', 'success', 'good', 'warning', 'severe', 'critical', 'error', 'bad', 'old'].includes(value)
34
+ ),
35
+ },
36
+ barLabel: {
37
+ type: String,
38
+ default: '',
39
+ },
40
+ align: {
41
+ type: String,
42
+ default: 'start',
43
+ validator: (value) => ['start', 'center'].includes(value),
44
+ },
45
+ compact: {
46
+ type: Boolean,
47
+ default: false,
48
+ },
49
+ to: {
50
+ type: [String, Object],
51
+ default: null,
52
+ },
53
+ href: {
54
+ type: String,
55
+ default: '',
56
+ },
57
+ })
58
+
59
+ const isLink = computed(() => Boolean(props.to || props.href))
60
+
61
+ const rootComponent = computed(() => {
62
+ if (props.to) {
63
+ return RouterLink
64
+ }
65
+ if (props.href) {
66
+ return 'a'
67
+ }
68
+ return 'div'
69
+ })
70
+
71
+ const rootBindings = computed(() => {
72
+ if (props.to) {
73
+ return { to: props.to }
74
+ }
75
+ if (props.href) {
76
+ return { href: props.href }
77
+ }
78
+ return {}
79
+ })
80
+
81
+ const rootClasses = computed(() => ({
82
+ 'status-card': true,
83
+ 'status-card--link': isLink.value,
84
+ [`status-card--${props.karma}`]: Boolean(props.karma),
85
+ [`status-card--align-${props.align}`]: true,
86
+ 'status-card--compact': props.compact,
87
+ }))
88
+ </script>
89
+
90
+ <style scoped>
91
+ .status-card {
92
+ display: flex;
93
+ align-items: stretch;
94
+ position: relative;
95
+ padding: 0;
96
+ overflow: hidden;
97
+ background: transparent;
98
+ border: 1px solid var(--nav-grid-border);
99
+ border-radius: 8px;
100
+ color: inherit;
101
+ text-align: left;
102
+ transition: all 0.2s ease;
103
+ }
104
+
105
+ a.status-card {
106
+ text-decoration: none;
107
+ cursor: pointer;
108
+ }
109
+
110
+ .status-card:hover {
111
+ border-color: var(--nav-grid-accent-fg);
112
+ background: var(--nav-grid-hover-bg);
113
+ color: var(--text-color);
114
+ transform: translateY(-2px);
115
+ box-shadow: 0 4px 8px var(--nav-grid-hover-shadow);
116
+ }
117
+
118
+ .status-card-bar {
119
+ position: relative;
120
+ flex: 0 0 0.35em;
121
+ width: 0.35em;
122
+ }
123
+
124
+ .status-card-bar--labeled {
125
+ flex-basis: 1.35em;
126
+ width: 1.35em;
127
+ }
128
+
129
+ .status-card-bar-label {
130
+ position: absolute;
131
+ top: 50%;
132
+ left: 50%;
133
+ transform: translate(-50%, -50%) rotate(270deg);
134
+ font-size: 0.65em;
135
+ font-weight: 600;
136
+ letter-spacing: 0.06em;
137
+ line-height: 1;
138
+ text-transform: uppercase;
139
+ white-space: nowrap;
140
+ color: var(--text-color);
141
+ user-select: none;
142
+ }
143
+
144
+ .status-card-body {
145
+ display: flex;
146
+ flex: 1;
147
+ flex-direction: column;
148
+ justify-content: flex-start;
149
+ gap: 0.25rem;
150
+ min-width: 0;
151
+ padding: 1.25rem 0.75rem;
152
+ }
153
+
154
+ .status-card--compact .status-card-body {
155
+ padding: 0.75rem 0.5rem;
156
+ gap: 0.15rem;
157
+ }
158
+
159
+ .status-card--align-start .status-card-body {
160
+ align-items: flex-start;
161
+ text-align: left;
162
+ }
163
+
164
+ .status-card--align-center .status-card-body {
165
+ align-items: center;
166
+ text-align: center;
167
+ }
168
+
169
+ .status-card-body :deep(h4) {
170
+ margin: 0;
171
+ }
172
+
173
+ .status-card-body :deep(.subtle) {
174
+ margin-top: 0.35rem;
175
+ }
176
+
177
+ .status-card--compact .status-card-body :deep(.subtle) {
178
+ margin-top: 0.2rem;
179
+ }
180
+
181
+ .status-card--good .status-card-bar,
182
+ .status-card--success .status-card-bar {
183
+ background-color: var(--karma-good-tint);
184
+ }
185
+
186
+ .status-card--bad .status-card-bar,
187
+ .status-card--critical .status-card-bar,
188
+ .status-card--error .status-card-bar {
189
+ background-color: var(--karma-bad-tint);
190
+ }
191
+
192
+ .status-card--warning .status-card-bar {
193
+ background-color: var(--karma-warning-tint);
194
+ }
195
+
196
+ .status-card--severe .status-card-bar {
197
+ background-color: var(--karma-severe-tint);
198
+ }
199
+
200
+ .status-card--note .status-card-bar {
201
+ background-color: var(--karma-note-tint);
202
+ }
203
+
204
+ .status-card--info .status-card-bar {
205
+ background-color: var(--karma-info-tint);
206
+ }
207
+
208
+ .status-card--old .status-card-bar {
209
+ background-color: var(--karma-old-tint);
210
+ }
211
+ </style>
@@ -8,67 +8,99 @@
8
8
  <div class="admin-section">
9
9
  <h3>System Overview</h3>
10
10
  <p>Welcome to the administrative panel. This is a placeholder for administrative functionality.</p>
11
+
12
+ <FormLayout @submit.prevent>
13
+ <FormField
14
+ label="Grid"
15
+ component-has-label
16
+ description="How many StatusCard columns to show."
17
+ >
18
+ <RadioGroup
19
+ v-model="gridLayout"
20
+ name="status-card-grid"
21
+ variant="boolean"
22
+ :options="gridLayoutOptions"
23
+ />
24
+ </FormField>
25
+
26
+ <FormField label="Content align" component-has-label>
27
+ <RadioGroup
28
+ v-model="cardAlign"
29
+ name="status-card-align"
30
+ variant="boolean"
31
+ :options="alignOptions"
32
+ />
33
+ </FormField>
34
+
35
+ <FormField label="Bar labels" component-has-label>
36
+ <RadioGroup
37
+ v-model="showBarLabels"
38
+ name="status-card-bar-labels"
39
+ variant="boolean"
40
+ :options="showHideOptions"
41
+ />
42
+ </FormField>
43
+
44
+ <FormField label="Compact" component-has-label>
45
+ <RadioGroup
46
+ v-model="cardCompact"
47
+ name="status-card-compact"
48
+ variant="boolean"
49
+ :options="onOffOptions"
50
+ />
51
+ </FormField>
52
+ </FormLayout>
11
53
 
12
- <div class="grid-boxed">
13
- <div class="stat-display good">
14
- <h4>Total Users</h4>
15
- <span class="stat">1,247</span>
16
- <span class="subtle">+12% from last month</span>
17
- </div>
18
-
19
- <div class="stat-display bad">
20
- <h4>Active Sessions</h4>
21
- <span class="stat">89</span>
22
- <span class="subtle">-3% from last hour</span>
23
- </div>
24
-
25
- <div class="stat-display warning">
26
- <h4>System Load</h4>
27
- <span class="stat">67%</span>
28
- <span class="subtle">Moderate</span>
29
- </div>
30
-
31
- <div class="stat-display good">
32
- <h4>Storage Used</h4>
33
- <span class="stat">2.4 GB</span>
34
- <span class="subtle">45% of total</span>
35
- </div>
54
+ <div :class="gridClass">
55
+ <StatusCard
56
+ v-for="card in overviewCards"
57
+ :key="card.title"
58
+ :karma="card.karma"
59
+ :bar-label="showBarLabels ? card.barLabel : ''"
60
+ :align="cardAlign"
61
+ :compact="cardCompact"
62
+ :to="card.to"
63
+ >
64
+ <h4>{{ card.title }}</h4>
65
+ <span class="stat">{{ card.stat }}</span>
66
+ <span class="subtle">{{ card.subtle }}</span>
67
+ </StatusCard>
36
68
  </div>
37
69
  </div>
38
70
 
39
71
  <div class="admin-section">
40
72
  <h3>Recent Activity</h3>
41
- <div class="activity-list">
42
- <div class="activity-item">
43
- <span class="activity-time">2 minutes ago</span>
44
- <span class="activity-text">User "john.doe" logged in</span>
45
- </div>
46
- <div class="activity-item">
47
- <span class="activity-time">5 minutes ago</span>
48
- <span class="activity-text">New user account created</span>
49
- </div>
50
- <div class="activity-item">
51
- <span class="activity-time">12 minutes ago</span>
52
- <span class="activity-text">System backup completed</span>
53
- </div>
54
- <div class="activity-item">
55
- <span class="activity-time">1 hour ago</span>
56
- <span class="activity-text">Database maintenance performed</span>
57
- </div>
58
- <div class="activity-item">
59
- <span class="activity-time">2 hours ago</span>
60
- <span class="activity-text">User "admin" updated settings</span>
61
- </div>
62
- </div>
73
+ <ul>
74
+ <li>
75
+ <span class="subtle">2 minutes ago</span>
76
+ — User "john.doe" logged in
77
+ </li>
78
+ <li>
79
+ <span class="subtle">5 minutes ago</span>
80
+ — New user account created
81
+ </li>
82
+ <li>
83
+ <span class="subtle">12 minutes ago</span>
84
+ — System backup completed
85
+ </li>
86
+ <li>
87
+ <span class="subtle">1 hour ago</span>
88
+ — Database maintenance performed
89
+ </li>
90
+ <li>
91
+ <span class="subtle">2 hours ago</span>
92
+ — User "admin" updated settings
93
+ </li>
94
+ </ul>
63
95
  </div>
64
96
 
65
97
  <div class="admin-section">
66
98
  <h3>Quick Actions</h3>
67
- <div role = "toolbar">
68
- <button class="action-btn">Create User</button>
69
- <button class="action-btn">System Backup</button>
70
- <button class="action-btn">View Logs</button>
71
- <button class="action-btn">Settings</button>
99
+ <div role="toolbar">
100
+ <button type="button">Create User</button>
101
+ <button type="button">System Backup</button>
102
+ <button type="button">View Logs</button>
103
+ <button type="button">Settings</button>
72
104
  </div>
73
105
  </div>
74
106
  </div>
@@ -76,6 +108,69 @@
76
108
  </template>
77
109
 
78
110
  <script setup>
79
- // This is a placeholder admin view with dummy content
80
- // No functionality implemented yet
111
+ import { computed, ref } from 'vue'
112
+
113
+ const showBarLabels = ref(true)
114
+ const cardCompact = ref(false)
115
+ const cardAlign = ref('start')
116
+ const gridLayout = ref('3')
117
+
118
+ const showHideOptions = [
119
+ { label: 'Shown', value: true },
120
+ { label: 'Hidden', value: false },
121
+ ]
122
+
123
+ const onOffOptions = [
124
+ { label: 'On', value: true },
125
+ { label: 'Off', value: false },
126
+ ]
127
+
128
+ const alignOptions = [
129
+ { value: 'start', label: 'Start' },
130
+ { value: 'center', label: 'Center' },
131
+ ]
132
+
133
+ const gridLayoutOptions = [
134
+ { value: '3', label: '3 columns' },
135
+ { value: '2', label: '2 columns' },
136
+ ]
137
+
138
+ const gridClass = computed(() => (
139
+ gridLayout.value === '2' ? 'grid-boxed-2' : 'grid-boxed'
140
+ ))
141
+
142
+ const overviewCards = [
143
+ {
144
+ karma: 'good',
145
+ barLabel: 'Users',
146
+ title: 'Total Users',
147
+ stat: '1,247',
148
+ subtle: '+12% from last month',
149
+ to: { name: 'UserDetails' },
150
+ },
151
+ {
152
+ karma: 'bad',
153
+ barLabel: 'Live',
154
+ title: 'Active Sessions',
155
+ stat: '89',
156
+ subtle: '-3% from last hour',
157
+ to: { name: 'TableExample' },
158
+ },
159
+ {
160
+ karma: 'warning',
161
+ barLabel: 'Load',
162
+ title: 'System Load',
163
+ stat: '67%',
164
+ subtle: 'Moderate',
165
+ to: { name: 'StatusExample' },
166
+ },
167
+ {
168
+ karma: 'good',
169
+ barLabel: 'Disk',
170
+ title: 'Storage Used',
171
+ stat: '2.4 GB',
172
+ subtle: '45% of total',
173
+ to: { name: 'ViewItem', params: { id: '1' } },
174
+ },
175
+ ]
81
176
  </script>