picocrank 1.9.0 → 1.10.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "picocrank",
3
- "version": "1.9.0",
3
+ "version": "1.10.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": {
@@ -18,14 +18,14 @@
18
18
  "url": "https://github.com/jamesread/picocrank/issues"
19
19
  },
20
20
  "dependencies": {
21
- "@hugeicons/core-free-icons": "^2.0.0",
22
- "@hugeicons/vue": "^1.0.3",
23
- "@vitejs/plugin-vue": "^6.0.2",
24
- "femtocrank": "^2.4.11",
21
+ "@hugeicons/core-free-icons": "^3.1.0",
22
+ "@hugeicons/vue": "^1.0.4",
23
+ "@vitejs/plugin-vue": "^6.0.3",
24
+ "femtocrank": "^2.4.12",
25
25
  "unplugin-vue-components": "^30.0.0",
26
- "vite": "^7.2.4",
27
- "vue": "^3.5.24",
28
- "vue-router": "^4.6.3"
26
+ "vite": "^7.3.0",
27
+ "vue": "^3.5.26",
28
+ "vue-router": "^4.6.4"
29
29
  },
30
30
  "devDependencies": {
31
31
  "semantic-release": "^25.0.2"
package/router.js CHANGED
@@ -5,6 +5,7 @@ import { TableIcon } from '@hugeicons/core-free-icons';
5
5
  import { ViewIcon } from '@hugeicons/core-free-icons';
6
6
  import { SecurityValidationIcon } from '@hugeicons/core-free-icons';
7
7
  import { CalendarIcon } from '@hugeicons/core-free-icons';
8
+ import { EditIcon } from '@hugeicons/core-free-icons';
8
9
 
9
10
  const routes = [
10
11
  {
@@ -54,6 +55,15 @@ const routes = [
54
55
  title: 'Calendar Example',
55
56
  icon: CalendarIcon,
56
57
  }
58
+ },
59
+ {
60
+ name: 'FormExample',
61
+ path: '/form-example',
62
+ component: () => import('./vue/views/FormExample.vue'),
63
+ meta: {
64
+ title: 'Form Example',
65
+ icon: EditIcon,
66
+ }
57
67
  }
58
68
  ];
59
69
 
@@ -66,4 +66,8 @@ button {
66
66
  button:hover {
67
67
  background-color: transparent;
68
68
  }
69
+
70
+ .user-info span {
71
+ padding-right: 0.5rem;
72
+ }
69
73
  </style>
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <!-- Navigation component has no visual output, it only manages navigation state -->
3
+ <slot />
4
+ </template>
5
+
6
+ <script setup>
7
+ import { ref, provide } from 'vue'
8
+ import { useRoute, useRouter } from 'vue-router'
9
+ import { PinIcon } from '@hugeicons/core-free-icons'
10
+
11
+ const navigationLinks = ref([])
12
+ const route = useRoute()
13
+ const router = useRouter()
14
+
15
+ /**
16
+ * Add a router link to the navigation
17
+ * @param {string} link - Route name
18
+ * @param {string|null} altTitle - Optional alternative title
19
+ * @param {object} options - Additional options
20
+ * @param {object} options.params - Route params to pass (e.g., { id: 1 })
21
+ * @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
22
+ * @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
23
+ */
24
+ function addRouterLink(link, altTitle = null, options = {}) {
25
+ const foundRoute = router.getRoutes().find(r => r.name === link)
26
+
27
+ if (!foundRoute) {
28
+ console.warn(`Route "${link}" not found`)
29
+ return
30
+ }
31
+
32
+ const title = altTitle || foundRoute.meta?.title || foundRoute.name
33
+
34
+ // Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
35
+ let toValue = foundRoute.path
36
+ if (options.to) {
37
+ toValue = options.to
38
+ } else if (options.params) {
39
+ toValue = { name: link, params: options.params }
40
+ }
41
+
42
+ const routeLink = {
43
+ name: link,
44
+ title: title,
45
+ path: foundRoute.path,
46
+ to: toValue,
47
+ icon: foundRoute.meta?.icon || PinIcon,
48
+ type: 'route',
49
+ props: options.props || {}
50
+ }
51
+
52
+ addNavigationLink(routeLink)
53
+ }
54
+
55
+ function addNavigationLink(link) {
56
+ const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
57
+ if (existingIndex >= 0) {
58
+ navigationLinks.value[existingIndex] = { ...link }
59
+ } else {
60
+ navigationLinks.value.push({ ...link })
61
+ }
62
+ }
63
+
64
+ function addCallback(title, callback, options = {}) {
65
+ const callbackLink = {
66
+ name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
67
+ type: 'callback',
68
+ icon: options.icon || PinIcon,
69
+ callback: callback || (() => {}),
70
+ title: title
71
+ }
72
+
73
+ addNavigationLink(callbackLink)
74
+ }
75
+
76
+ function addSeparator(id) {
77
+ const separator = {
78
+ name: id || `nav-separator-${Date.now()}`,
79
+ type: 'separator',
80
+ title: 'Separator'
81
+ }
82
+ addNavigationLink(separator)
83
+ }
84
+
85
+ function addHtml(html, options = {}) {
86
+ const htmlLink = {
87
+ name: options.name || `html-item-${Date.now()}`,
88
+ type: 'html',
89
+ html: html,
90
+ title: options.title || 'HTML Item'
91
+ }
92
+ addNavigationLink(htmlLink)
93
+ }
94
+
95
+ function removeNavigationLink(linkId) {
96
+ navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
97
+ }
98
+
99
+ function clearNavigationLinks() {
100
+ navigationLinks.value = []
101
+ }
102
+
103
+ function getNavigationLinks() {
104
+ return [...navigationLinks.value]
105
+ }
106
+
107
+ function isActive(link) {
108
+ // If link has a 'to' object with a name, compare route name and params
109
+ if (link.to && typeof link.to === 'object' && link.to.name) {
110
+ // Compare route names first
111
+ if (route.name !== link.to.name) {
112
+ return false
113
+ }
114
+
115
+ // If params are specified in the link, all specified params must match
116
+ if (link.to.params && Object.keys(link.to.params).length > 0) {
117
+ const linkParams = link.to.params
118
+ const routeParams = route.params
119
+
120
+ // Check if all params in link.to.params match route.params
121
+ for (const key in linkParams) {
122
+ if (String(routeParams[key]) !== String(linkParams[key])) {
123
+ return false
124
+ }
125
+ }
126
+ }
127
+
128
+ // Route name matches, and either no params specified or all params match
129
+ return true
130
+ }
131
+
132
+ // Fall back to path comparison for string paths
133
+ const linkPath = typeof link.to === 'string' ? link.to : link.path
134
+ return route.path === linkPath
135
+ }
136
+
137
+ // Provide navigation API for child components
138
+ provide('navigation', {
139
+ navigationLinks,
140
+ addRouterLink,
141
+ addNavigationLink,
142
+ addCallback,
143
+ addSeparator,
144
+ addHtml,
145
+ removeNavigationLink,
146
+ clearNavigationLinks,
147
+ getNavigationLinks,
148
+ isActive
149
+ })
150
+
151
+ // Expose API for direct ref access
152
+ defineExpose({
153
+ navigationLinks,
154
+ addRouterLink,
155
+ addNavigationLink,
156
+ addCallback,
157
+ addSeparator,
158
+ addHtml,
159
+ removeNavigationLink,
160
+ clearNavigationLinks,
161
+ getNavigationLinks,
162
+ isActive
163
+ })
164
+ </script>
165
+
@@ -303,7 +303,6 @@ defineExpose({
303
303
  .quick-search {
304
304
  position: relative;
305
305
  width: 100%;
306
- max-width: 400px;
307
306
  }
308
307
 
309
308
  .search-container {
@@ -370,7 +369,7 @@ defineExpose({
370
369
 
371
370
  .search-result-item:hover,
372
371
  .search-result-item.active {
373
- background-color: #f8fafc;
372
+ background-color: var(--hover-background-color);
374
373
  }
375
374
 
376
375
  .result-content {
@@ -36,111 +36,21 @@
36
36
  </template>
37
37
 
38
38
  <script setup>
39
- import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
40
- import { useRoute, useRouter } from 'vue-router'
39
+ import { ref, inject, onUnmounted, watch, nextTick } from 'vue'
41
40
  import { HugeiconsIcon } from '@hugeicons/vue'
42
41
  import { Pin02Icon } from '@hugeicons/core-free-icons'
43
42
  import { PinIcon } from '@hugeicons/core-free-icons'
44
43
 
45
44
  const isOpen = ref(false)
46
45
  const isStuck = ref(false)
47
- const navigationLinks = ref([])
48
46
  const sidebarRef = ref(null)
49
47
 
50
- const route = useRoute()
51
- const router = useRouter()
48
+ // Get navigation from Navigation component
49
+ const navigation = inject('navigation', null)
52
50
 
53
- /**
54
- * Add a router link to the sidebar navigation
55
- * @param {string} link - Route name
56
- * @param {string|null} altTitle - Optional alternative title
57
- * @param {object} options - Additional options
58
- * @param {object} options.params - Route params to pass (e.g., { id: 1 })
59
- * @param {object} options.props - Props to pass to router-link (e.g., { replace: true, custom: true })
60
- * @param {object|string} options.to - Override the 'to' prop (if provided, takes precedence over params)
61
- */
62
- function addRouterLink(link, altTitle = null, options = {}) {
63
- const foundRoute = router.getRoutes().find(r => r.name === link)
64
-
65
- if (!foundRoute) {
66
- console.warn(`Route "${link}" not found`)
67
- return
68
- }
69
-
70
- const title = altTitle || foundRoute.meta?.title || foundRoute.name
71
-
72
- // Determine the 'to' value: use options.to if provided, otherwise construct from params, or fall back to path
73
- let toValue = foundRoute.path
74
- if (options.to) {
75
- toValue = options.to
76
- } else if (options.params) {
77
- toValue = { name: link, params: options.params }
78
- }
79
-
80
- const routeLink = {
81
- name: link,
82
- title: title,
83
- path: foundRoute.path,
84
- to: toValue,
85
- icon: foundRoute.meta?.icon || PinIcon,
86
- type: 'route',
87
- props: options.props || {}
88
- }
89
-
90
- addNavigationLink(routeLink)
91
- }
92
-
93
- function addNavigationLink(link) {
94
- const existingIndex = navigationLinks.value.findIndex(l => l.name === link.name)
95
- if (existingIndex >= 0) {
96
- navigationLinks.value[existingIndex] = { ...link }
97
- } else {
98
- navigationLinks.value.push({ ...link })
99
- }
100
- }
101
-
102
- function addCallback(title, callback, options = {}) {
103
- const callbackLink = {
104
- name: options.name || title.toLowerCase().replace(/\s+/g, '-'),
105
- type: 'callback',
106
- icon: options.icon || PinIcon,
107
- callback: callback || (() => {}),
108
- title: title
109
- }
110
-
111
- addNavigationLink(callbackLink)
112
- }
113
-
114
- function addSeparator(id) {
115
- const separator = {
116
- name: id || `nav-separator-${Date.now()}`,
117
- type: 'separator',
118
- title: 'Separator'
119
- }
120
- addNavigationLink(separator)
121
- }
122
-
123
- function addHtml(html, options = {}) {
124
- const htmlLink = {
125
- name: options.name || `html-item-${Date.now()}`,
126
- type: 'html',
127
- html: html,
128
- title: options.title || 'HTML Item'
129
- }
130
- addNavigationLink(htmlLink)
131
- }
132
-
133
- function removeNavigationLink(linkId) {
134
- navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
135
- }
136
-
137
- function clearNavigationLinks() {
138
- navigationLinks.value = []
139
- }
140
-
141
- function getNavigationLinks() {
142
- return [...navigationLinks.value]
143
- }
51
+ // Get navigation links and isActive function from navigation
52
+ const navigationLinks = navigation ? navigation.navigationLinks : ref([])
53
+ const isActive = navigation ? navigation.isActive : (() => false)
144
54
 
145
55
  function toggleStick() {
146
56
  isStuck.value = !isStuck.value
@@ -168,36 +78,6 @@ function close() {
168
78
  isStuck.value = false
169
79
  }
170
80
 
171
- function isActive(link) {
172
- // If link has a 'to' object with a name, compare route name and params
173
- if (link.to && typeof link.to === 'object' && link.to.name) {
174
- // Compare route names first
175
- if (route.name !== link.to.name) {
176
- return false
177
- }
178
-
179
- // If params are specified in the link, all specified params must match
180
- if (link.to.params && Object.keys(link.to.params).length > 0) {
181
- const linkParams = link.to.params
182
- const routeParams = route.params
183
-
184
- // Check if all params in link.to.params match route.params
185
- for (const key in linkParams) {
186
- if (String(routeParams[key]) !== String(linkParams[key])) {
187
- return false
188
- }
189
- }
190
- }
191
-
192
- // Route name matches, and either no params specified or all params match
193
- return true
194
- }
195
-
196
- // Fall back to path comparison for string paths
197
- const linkPath = typeof link.to === 'string' ? link.to : link.path
198
- return route.path === linkPath
199
- }
200
-
201
81
  function handleLinkClick(callback = null) {
202
82
  if (callback) {
203
83
  callback()
@@ -255,22 +135,12 @@ onUnmounted(() => {
255
135
 
256
136
  defineExpose({
257
137
  isOpen,
258
- navigationLinks,
259
138
  stick,
260
139
  unstick,
261
140
  toggleStick,
262
141
  toggle,
263
142
  open,
264
143
  close,
265
- isActive,
266
- addNavigationLink,
267
- addRouterLink,
268
- addCallback,
269
- addSeparator,
270
- addHtml,
271
- removeNavigationLink,
272
- clearNavigationLinks,
273
- getNavigationLinks,
274
144
  })
275
145
  </script>
276
146
 
@@ -33,7 +33,7 @@
33
33
  </tr>
34
34
  </tbody>
35
35
  </table>
36
- <div class = "padding">
36
+ <div v-if="showPagination" class = "padding">
37
37
  <Pagination :total = "total" v-model:page="page" v-model:page-size="pageSize" />
38
38
  </div>
39
39
  </template>
@@ -56,6 +56,10 @@
56
56
  type: Array,
57
57
  default: () => [],
58
58
  },
59
+ showPagination: {
60
+ type: Boolean,
61
+ default: true,
62
+ },
59
63
  });
60
64
 
61
65
  const slots = useSlots()
package/vue/views/App.vue CHANGED
@@ -10,19 +10,21 @@
10
10
  </template>
11
11
  </Header>
12
12
 
13
- <div id = "layout">
14
- <Sidebar v-if="sidebarEnabled" ref = "sidebar" />
13
+ <Navigation ref="navigation">
14
+ <div id = "layout">
15
+ <Sidebar v-if="sidebarEnabled" ref = "sidebar" />
15
16
 
16
- <div id = "content">
17
- <main>
18
- <router-view />
19
- </main>
17
+ <div id = "content">
18
+ <main>
19
+ <router-view />
20
+ </main>
20
21
 
21
- <footer>
22
- <span><a href = "https://github.com/jamesread/picocrank">PicoCrank</a></span>
23
- </footer>
22
+ <footer>
23
+ <span><a href = "https://github.com/jamesread/picocrank">PicoCrank</a></span>
24
+ </footer>
25
+ </div>
24
26
  </div>
25
- </div>
27
+ </Navigation>
26
28
  </template>
27
29
 
28
30
  <script setup>
@@ -31,12 +33,14 @@
31
33
  import { Pin02Icon } from '@hugeicons/core-free-icons'
32
34
 
33
35
  import QuickSearch from './../components/QuickSearch.vue'
36
+ import Navigation from './../components/Navigation.vue'
34
37
  import '../../styles.css'
35
38
  import logoUrl from '/logo.png';
36
39
 
37
40
  const quickSearchRef = ref(null)
38
41
  const router = useRouter();
39
42
  const sidebar = ref(null);
43
+ const navigation = ref(null);
40
44
  const sidebarEnabled = ref(true);
41
45
 
42
46
  function toggleSidebar() {
@@ -54,17 +58,21 @@
54
58
  provide('toggleSidebarEnabled', toggleSidebarEnabled);
55
59
 
56
60
  onMounted(() => {
61
+ if (navigation.value) {
62
+ navigation.value.addRouterLink('Welcome')
63
+ navigation.value.addRouterLink('TableExample')
64
+ navigation.value.addRouterLink('CalendarExample')
65
+ navigation.value.addRouterLink('FormExample')
66
+ navigation.value.addSeparator('separator-1');
67
+ navigation.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
68
+ navigation.value.addSeparator('separator-2');
69
+ navigation.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
70
+ navigation.value.addSeparator('separator-3');
71
+ navigation.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
72
+ navigation.value.addRouterLink('Admin')
73
+ }
74
+
57
75
  if (sidebar.value) {
58
- sidebar.value.addRouterLink('Welcome')
59
- sidebar.value.addRouterLink('TableExample')
60
- sidebar.value.addRouterLink('CalendarExample')
61
- sidebar.value.addSeparator('separator-1');
62
- sidebar.value.addRouterLink('ViewItem', 'View Awesome Item', { params: { id: 1 } })
63
- sidebar.value.addSeparator('separator-2');
64
- sidebar.value.addCallback('Callback Example', helloWorld, { icon: Pin02Icon })
65
- sidebar.value.addSeparator('separator-3');
66
- sidebar.value.addHtml('<h2 style = "padding: 0.75em;">Administration</h2>', { name: 'admin-heading' })
67
- sidebar.value.addRouterLink('Admin')
68
76
  sidebar.value.open();
69
77
  sidebar.value.stick();
70
78
  }
@@ -0,0 +1,268 @@
1
+ <template>
2
+ <Section
3
+ title="Form Example"
4
+ subtitle="A simple form with various input types"
5
+ >
6
+ <template #toolbar>
7
+ <button @click="resetForm">Reset</button>
8
+ </template>
9
+
10
+ <form @submit.prevent="handleSubmit">
11
+ <div class="form-group">
12
+ <label for="name">Name</label>
13
+ <input
14
+ id="name"
15
+ v-model="formData.name"
16
+ type="text"
17
+ placeholder="Enter your name"
18
+ required
19
+ />
20
+ </div>
21
+
22
+ <div class="form-group">
23
+ <label for="email">Email</label>
24
+ <input
25
+ id="email"
26
+ v-model="formData.email"
27
+ type="email"
28
+ placeholder="Enter your email"
29
+ required
30
+ />
31
+ </div>
32
+
33
+ <div class="form-group">
34
+ <label for="country">Country</label>
35
+ <select id="country" v-model="formData.country" required>
36
+ <option value="">Select a country</option>
37
+ <option value="us">United States</option>
38
+ <option value="uk">United Kingdom</option>
39
+ <option value="ca">Canada</option>
40
+ <option value="au">Australia</option>
41
+ <option value="de">Germany</option>
42
+ <option value="fr">France</option>
43
+ </select>
44
+ </div>
45
+
46
+ <div class="form-group">
47
+ <label for="message">Message</label>
48
+ <textarea
49
+ id="message"
50
+ v-model="formData.message"
51
+ placeholder="Enter your message"
52
+ rows="4"
53
+ ></textarea>
54
+ </div>
55
+
56
+ <div class="form-group">
57
+ <label>
58
+ <input
59
+ type="checkbox"
60
+ v-model="formData.newsletter"
61
+ />
62
+ Subscribe to newsletter
63
+ </label>
64
+ </div>
65
+
66
+ <div class="form-group">
67
+ <label>Preferred Contact Method</label>
68
+ <div class="radio-group">
69
+ <label>
70
+ <input
71
+ type="radio"
72
+ v-model="formData.contactMethod"
73
+ value="email"
74
+ />
75
+ Email
76
+ </label>
77
+ <label>
78
+ <input
79
+ type="radio"
80
+ v-model="formData.contactMethod"
81
+ value="phone"
82
+ />
83
+ Phone
84
+ </label>
85
+ <label>
86
+ <input
87
+ type="radio"
88
+ v-model="formData.contactMethod"
89
+ value="mail"
90
+ />
91
+ Mail
92
+ </label>
93
+ </div>
94
+ </div>
95
+
96
+ <div class="form-actions">
97
+ <button type="submit">Submit</button>
98
+ <button type="button" @click="resetForm">Clear</button>
99
+ </div>
100
+ </form>
101
+
102
+ <div v-if="submitted" class="form-result">
103
+ <h3>Form Submitted!</h3>
104
+ <pre>{{ JSON.stringify(formData, null, 2) }}</pre>
105
+ </div>
106
+ </Section>
107
+ </template>
108
+
109
+ <script setup>
110
+ import { ref } from 'vue'
111
+ import Section from '../components/Section.vue'
112
+
113
+ const formData = ref({
114
+ name: '',
115
+ email: '',
116
+ country: '',
117
+ message: '',
118
+ newsletter: false,
119
+ contactMethod: 'email'
120
+ })
121
+
122
+ const submitted = ref(false)
123
+
124
+ function handleSubmit() {
125
+ submitted.value = true
126
+ console.log('Form submitted:', formData.value)
127
+ // In a real application, you would send this data to a server
128
+ alert('Form submitted! Check the console and the form result below.')
129
+ }
130
+
131
+ function resetForm() {
132
+ formData.value = {
133
+ name: '',
134
+ email: '',
135
+ country: '',
136
+ message: '',
137
+ newsletter: false,
138
+ contactMethod: 'email'
139
+ }
140
+ submitted.value = false
141
+ }
142
+ </script>
143
+
144
+ <style scoped>
145
+ .form-group {
146
+ margin-bottom: 1.5rem;
147
+ }
148
+
149
+ .form-group label {
150
+ display: block;
151
+ margin-bottom: 0.5rem;
152
+ font-weight: 500;
153
+ }
154
+
155
+ .form-group input[type="text"],
156
+ .form-group input[type="email"],
157
+ .form-group select,
158
+ .form-group textarea {
159
+ width: 100%;
160
+ padding: 0.5rem;
161
+ border: 1px solid #ccc;
162
+ border-radius: 0.25rem;
163
+ font-size: 1rem;
164
+ }
165
+
166
+ .form-group textarea {
167
+ resize: vertical;
168
+ font-family: inherit;
169
+ }
170
+
171
+ .form-group input[type="checkbox"],
172
+ .form-group input[type="radio"] {
173
+ margin-right: 0.5rem;
174
+ }
175
+
176
+ .form-group label:has(input[type="checkbox"]) {
177
+ display: flex;
178
+ align-items: center;
179
+ font-weight: normal;
180
+ }
181
+
182
+ .radio-group {
183
+ display: flex;
184
+ gap: 1.5rem;
185
+ flex-wrap: wrap;
186
+ }
187
+
188
+ .radio-group label {
189
+ display: flex;
190
+ align-items: center;
191
+ font-weight: normal;
192
+ }
193
+
194
+ .form-actions {
195
+ display: flex;
196
+ gap: 1rem;
197
+ margin-top: 2rem;
198
+ }
199
+
200
+ .form-actions button {
201
+ padding: 0.75rem 1.5rem;
202
+ border: none;
203
+ border-radius: 0.25rem;
204
+ font-size: 1rem;
205
+ cursor: pointer;
206
+ }
207
+
208
+ .form-actions button[type="submit"] {
209
+ background-color: #0366d6;
210
+ color: white;
211
+ }
212
+
213
+ .form-actions button[type="submit"]:hover {
214
+ background-color: #0256c2;
215
+ }
216
+
217
+ .form-actions button[type="button"] {
218
+ background-color: #6c757d;
219
+ color: white;
220
+ }
221
+
222
+ .form-actions button[type="button"]:hover {
223
+ background-color: #5a6268;
224
+ }
225
+
226
+ .form-result {
227
+ margin-top: 2rem;
228
+ padding: 1rem;
229
+ background-color: #f8f9fa;
230
+ border-radius: 0.25rem;
231
+ border: 1px solid #dee2e6;
232
+ }
233
+
234
+ .form-result h3 {
235
+ margin-top: 0;
236
+ margin-bottom: 1rem;
237
+ }
238
+
239
+ .form-result pre {
240
+ margin: 0;
241
+ padding: 1rem;
242
+ background-color: white;
243
+ border-radius: 0.25rem;
244
+ overflow-x: auto;
245
+ }
246
+
247
+ @media (prefers-color-scheme: dark) {
248
+ .form-group input[type="text"],
249
+ .form-group input[type="email"],
250
+ .form-group select,
251
+ .form-group textarea {
252
+ background-color: #333;
253
+ border-color: #555;
254
+ color: #fff;
255
+ }
256
+
257
+ .form-result {
258
+ background-color: #2d2d2d;
259
+ border-color: #444;
260
+ }
261
+
262
+ .form-result pre {
263
+ background-color: #1a1a1a;
264
+ color: #fff;
265
+ }
266
+ }
267
+ </style>
268
+