picocrank 1.0.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.
@@ -0,0 +1,36 @@
1
+ name: Release
2
+ on:
3
+ push:
4
+ branches:
5
+ - main # or main
6
+
7
+ permissions:
8
+ contents: read # for checkout
9
+
10
+ jobs:
11
+ release:
12
+ name: Release
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ contents: write # to be able to publish a GitHub release
16
+ issues: write # to be able to comment on released issues
17
+ pull-requests: write # to be able to comment on released pull requests
18
+ id-token: write # to enable use of OIDC for npm provenance
19
+ steps:
20
+ - name: Checkout
21
+ uses: actions/checkout@v4
22
+ with:
23
+ fetch-depth: 0
24
+ - name: Setup Node.js
25
+ uses: actions/setup-node@v4
26
+ with:
27
+ node-version: "lts/*"
28
+ - name: Install dependencies
29
+ run: npm clean-install
30
+ - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
31
+ run: npm audit signatures
32
+ - name: Release
33
+ env:
34
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
36
+ run: npx semantic-release
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ <div align = "center">
2
+ <img alt = "project logo" src = "logo.png" width = "128" />
3
+ <h1>PicoCrank</h1>
4
+
5
+ CSS theme for high accessibility and zero dependencies
6
+
7
+ [![Maturity Badge](https://img.shields.io/badge/maturity-Production-brightgreen)](#none)
8
+ [![Discord](https://img.shields.io/discord/846737624960860180?label=Discord%20Server)](https://discord.gg/jhYWWpNJ3v)
9
+
10
+ </div>
11
+
12
+ A set of zero dependency Vue components built on the FemtoCrank global CSS theme.
package/index.html ADDED
@@ -0,0 +1,20 @@
1
+ <html>
2
+ <head>
3
+ <title>PicoCrank</title>
4
+ </head>
5
+
6
+ <body>
7
+ <slot id = "app" />
8
+ </body>
9
+
10
+ <script type = "module">
11
+ import 'femtocrank/style.css';
12
+ import { createApp } from 'vue';
13
+ import App from './vue/views/App.vue';
14
+ import router from './router.js';
15
+
16
+ const app = createApp(App)
17
+ app.use(router)
18
+ app.mount('#app');
19
+ </script>
20
+ </html>
package/logo.png ADDED
Binary file
package/logo.svg ADDED
@@ -0,0 +1,53 @@
1
+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
+ <svg
3
+ width="24"
4
+ height="24"
5
+ viewBox="0 0 24 24"
6
+ version="1.1"
7
+ id="svg2"
8
+ sodipodi:docname="logo.svg"
9
+ inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
10
+ inkscape:export-filename="logo.png"
11
+ inkscape:export-xdpi="2048"
12
+ inkscape:export-ydpi="2048"
13
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
14
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ xmlns:svg="http://www.w3.org/2000/svg">
17
+ <defs
18
+ id="defs2" />
19
+ <sodipodi:namedview
20
+ id="namedview2"
21
+ pagecolor="#ffffff"
22
+ bordercolor="#111111"
23
+ borderopacity="1"
24
+ inkscape:showpageshadow="0"
25
+ inkscape:pageopacity="0"
26
+ inkscape:pagecheckerboard="1"
27
+ inkscape:deskcolor="#d1d1d1"
28
+ inkscape:zoom="22.627417"
29
+ inkscape:cx="35.465825"
30
+ inkscape:cy="-0.41984466"
31
+ inkscape:window-width="3840"
32
+ inkscape:window-height="2126"
33
+ inkscape:window-x="2160"
34
+ inkscape:window-y="2194"
35
+ inkscape:window-maximized="1"
36
+ inkscape:current-layer="g2" />
37
+ <g
38
+ fill="none"
39
+ stroke="currentColor"
40
+ stroke-linecap="round"
41
+ stroke-linejoin="round"
42
+ stroke-width="1.5"
43
+ id="g2">
44
+ <path
45
+ d="M21.011 14.097c.522-.141.783-.212.886-.346c.103-.135.103-.351.103-.784v-1.934c0-.433 0-.65-.103-.784s-.364-.205-.886-.345c-1.95-.526-3.171-2.565-2.668-4.503c.139-.533.208-.8.142-.956s-.256-.264-.635-.479l-1.725-.98c-.372-.21-.558-.316-.725-.294s-.356.21-.733.587c-1.459 1.455-3.873 1.455-5.333 0c-.377-.376-.565-.564-.732-.587c-.167-.022-.353.083-.725.295l-1.725.979c-.38.215-.57.323-.635.48c-.066.155.003.422.141.955c.503 1.938-.718 3.977-2.669 4.503c-.522.14-.783.21-.886.345S2 10.6 2 11.033v1.934c0 .433 0 .65.103.784s.364.205.886.346c1.95.526 3.171 2.565 2.668 4.502c-.139.533-.208.8-.142.956s.256.264.635.48l1.725.978c.372.212.558.317.725.295s.356-.21.733-.587c1.46-1.457 3.876-1.457 5.336 0c.377.376.565.564.732.587c.167.022.353-.083.726-.295l1.724-.979c.38-.215.57-.323.635-.48s-.003-.422-.141-.955c-.504-1.937.716-3.976 2.666-4.502"
46
+ id="path2"
47
+ style="fill:#42b883;fill-opacity:1" />
48
+ <path
49
+ d="m 15.63442,11.75922 a 3.5,3.5 0 1 1 -6.9999999,0 3.5,3.5 0 0 1 6.9999999,0"
50
+ id="path1"
51
+ style="fill:#35495e;fill-opacity:1" />
52
+ </g>
53
+ </svg>
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "picocrank",
3
+ "version": "1.0.0",
4
+ "description": "A set of zero dependency Vue components built on the Femtocrank global CSS theme.",
5
+ "main": "vite.config.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/jamesread/picocrank.git"
12
+ },
13
+ "author": "",
14
+ "license": "ISC",
15
+ "bugs": {
16
+ "url": "https://github.com/jamesread/picocrank/issues"
17
+ },
18
+ "dependencies": {
19
+ "@hugeicons/core-free-icons": "^1.0.16",
20
+ "@hugeicons/vue": "^1.0.3",
21
+ "@vitejs/plugin-vue": "^6.0.1",
22
+ "femtocrank": "^2.2.0",
23
+ "unplugin-vue-components": "^29.0.0",
24
+ "vite": "^6.3.5",
25
+ "vue": "^3.5.18",
26
+ "vue-router": "^4.5.1"
27
+ },
28
+ "devDependencies": {
29
+ "semantic-release": "^24.2.7"
30
+ },
31
+ "homepage": "https://github.com/jamesread/picocrank#readme"
32
+ }
package/router.js ADDED
@@ -0,0 +1,18 @@
1
+ import { createRouter, createWebHistory } from 'vue-router';
2
+
3
+ import Welcome from './vue/views/Welcome.vue';
4
+
5
+ const routes = [
6
+ {
7
+ path: '/',
8
+ name: 'Home',
9
+ component: Welcome,
10
+ }
11
+ ];
12
+
13
+ const router = createRouter({
14
+ history: createWebHistory(),
15
+ routes,
16
+ });
17
+
18
+ export default router
package/vite.config.js ADDED
@@ -0,0 +1,29 @@
1
+ import { defineConfig } from 'vite'
2
+ import vue from '@vitejs/plugin-vue'
3
+ import Components from 'unplugin-vue-components/vite'
4
+
5
+ export default defineConfig({
6
+ server: {
7
+ proxy: {
8
+ '/api': {
9
+ target: 'http://localhost:8080',
10
+ changeOrigin: true,
11
+ secure: false,
12
+ },
13
+ '/lang': {
14
+ target: 'http://localhost:8080',
15
+ changeOrigin: true,
16
+ secure: false,
17
+ }
18
+ },
19
+ },
20
+ plugins: [
21
+ Components({
22
+ dirs: "./vue/",
23
+ extensions: ['vue'],
24
+ deep: true,
25
+ dts: false,
26
+ }),
27
+ vue(),
28
+ ],
29
+ })
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <div id = "breadcrumbs">
3
+ <template v-for="(link, index) in links" :key="link.name">
4
+ <router-link :to="link.href">{{ link.name }}</router-link>
5
+ <span v-if="index < links.length - 1" class="separator">
6
+ &raquo;
7
+ </span>
8
+ </template>
9
+ </div>
10
+ </template>
11
+
12
+ <style scoped>
13
+ span {
14
+ color: #bbb;
15
+ }
16
+
17
+ a {
18
+ text-decoration: none;
19
+ padding: 0.4em;
20
+ border-radius: 0.2em;
21
+ }
22
+
23
+ a:hover {
24
+ text-decoration: underline;
25
+ background-color: #000;
26
+ }
27
+
28
+ </style>
29
+
30
+
31
+ <script setup>
32
+ import { ref } from 'vue';
33
+ import { watch } from 'vue';
34
+ import { useRoute } from 'vue-router';
35
+
36
+ const route = useRoute();
37
+ const links = ref([]);
38
+
39
+ watch(() => route.matched, (matched) => {
40
+
41
+ links.value = [];
42
+ matched.forEach((record) => {
43
+ if (record.meta && record.meta.breadcrumbs) {
44
+ record.meta.breadcrumbs(route).forEach((item) => {
45
+ links.value.push({
46
+ name: item.name,
47
+ href: item.href || record.path || '/'
48
+ });
49
+ });
50
+ } else if (record.name) {
51
+ links.value.push({
52
+ name: record.name,
53
+ href: record.path || '/'
54
+ });
55
+ }
56
+ });
57
+ }, { immediate: true });
58
+ </script>
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <header>
3
+ <div class = "image-and-title flex-row">
4
+ <img src = "/logo.png" alt = "Logo" class = "logo" />
5
+ <h1>PicoCrank</h1>
6
+ </div>
7
+ </header>
8
+ </template>
@@ -0,0 +1,273 @@
1
+ <template>
2
+ <div class="pagination">
3
+ <div class="pagination-info">
4
+ <span class="pagination-text">
5
+ Showing {{ startItem + 1 }}-{{ endItem }} of {{ total }} {{ itemTitle }}
6
+ </span>
7
+ </div>
8
+
9
+ <div class="pagination-controls">
10
+ <button
11
+ class="pagination-btn"
12
+ :disabled="currentPage === 1"
13
+ @click="goToPage(currentPage - 1)"
14
+ title="Previous page"
15
+ >
16
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
17
+ <path fill="currentColor" d="M15.41 7.41L14 6l-6 6l6 6l1.41-1.41L10.83 12z"/>
18
+ </svg>
19
+ </button>
20
+
21
+
22
+ <div class="pagination-pages">
23
+ <button
24
+ v-if="showFirstPage"
25
+ class="pagination-btn"
26
+ :class="{ active: currentPage === 1 }"
27
+ @click="goToPage(1)"
28
+ >
29
+ 1
30
+ </button>
31
+
32
+ <span v-if="showFirstEllipsis" class="pagination-ellipsis">...</span>
33
+
34
+ <button
35
+ v-for="page in visiblePages"
36
+ :key="page"
37
+ class="pagination-btn"
38
+ :class="{ active: currentPage === page }"
39
+ @click="goToPage(page)"
40
+ >
41
+ {{ page }}
42
+ </button>
43
+
44
+ <span v-if="showLastEllipsis" class="pagination-ellipsis">...</span>
45
+
46
+ <button
47
+ v-if="showLastPage"
48
+ class="pagination-btn"
49
+ :class="{ active: currentPage === totalPages }"
50
+ @click="goToPage(totalPages)"
51
+ >
52
+ {{ totalPages }}
53
+ </button>
54
+ </div>
55
+
56
+ <button
57
+ class="pagination-btn"
58
+ :disabled="currentPage === totalPages"
59
+ @click="goToPage(currentPage + 1)"
60
+ title="Next page"
61
+ >
62
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24">
63
+ <path fill="currentColor" d="M8.59 16.59L10 18l6-6l-6-6L8.59 7.41L13.17 12z"/>
64
+ </svg>
65
+ </button>
66
+ </div>
67
+
68
+ <div class="pagination-size" v-if="canChangePageSize">
69
+ <label for="page-size">Items per page:</label>
70
+ <select
71
+ id="page-size"
72
+ v-model="localPageSize"
73
+ @change="handlePageSizeChange"
74
+ class="page-size-select"
75
+ >
76
+ <option value="10">10</option>
77
+ <option value="25">25</option>
78
+ <option value="50">50</option>
79
+ <option value="100">100</option>
80
+ </select>
81
+ </div>
82
+ </div>
83
+ </template>
84
+
85
+ <script setup>
86
+ import { ref, computed, watch } from 'vue'
87
+
88
+ const props = defineProps({
89
+ pageSize: {
90
+ type: Number,
91
+ default: 25
92
+ },
93
+ total: {
94
+ type: Number,
95
+ required: true
96
+ },
97
+ currentPage: {
98
+ type: Number,
99
+ default: 1
100
+ },
101
+ canChangePageSize: {
102
+ type: Boolean,
103
+ default: false
104
+ },
105
+ itemTitle: {
106
+ type: String,
107
+ default: 'items'
108
+ }
109
+ })
110
+
111
+ const emit = defineEmits(['page-change', 'page-size-change'])
112
+
113
+ const localPageSize = ref(props.pageSize)
114
+ const localCurrentPage = ref(props.currentPage)
115
+
116
+ const totalPages = computed(() => Math.ceil(props.total / localPageSize.value))
117
+
118
+ const startItem = computed(() => (localCurrentPage.value - 1) * localPageSize.value)
119
+ const endItem = computed(() => Math.min(localCurrentPage.value * localPageSize.value, props.total))
120
+
121
+ const maxVisiblePages = 5
122
+ const visiblePages = computed(() => {
123
+ const pages = []
124
+ const halfVisible = Math.floor(maxVisiblePages / 2)
125
+
126
+ let start = Math.max(1, localCurrentPage.value - halfVisible)
127
+ let end = Math.min(totalPages.value, start + maxVisiblePages - 1)
128
+
129
+ if (end - start < maxVisiblePages - 1) {
130
+ start = Math.max(1, end - maxVisiblePages + 1)
131
+ }
132
+
133
+ for (let i = start; i <= end; i++) {
134
+ pages.push(i)
135
+ }
136
+
137
+ return pages
138
+ })
139
+
140
+ const showFirstPage = computed(() => visiblePages.value[0] > 1)
141
+ const showLastPage = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value)
142
+ const showFirstEllipsis = computed(() => visiblePages.value[0] > 2)
143
+ const showLastEllipsis = computed(() => visiblePages.value[visiblePages.value.length - 1] < totalPages.value - 1)
144
+
145
+ function goToPage(page) {
146
+ if (page >= 1 && page <= totalPages.value && page !== localCurrentPage.value) {
147
+ localCurrentPage.value = page
148
+ emit('page-change', page)
149
+ }
150
+ }
151
+
152
+ function handlePageSizeChange() {
153
+ localCurrentPage.value = 1
154
+ emit('page-size-change', localPageSize.value)
155
+ emit('page-change', 1)
156
+ }
157
+
158
+ watch(() => props.currentPage, (newPage) => {
159
+ localCurrentPage.value = newPage
160
+ })
161
+
162
+ watch(() => props.pageSize, (newSize) => {
163
+ localPageSize.value = newSize
164
+ })
165
+ </script>
166
+
167
+ <style scoped>
168
+ .pagination {
169
+ display: flex;
170
+ align-items: center;
171
+ justify-content: space-between;
172
+ margin-top: 1rem;
173
+ }
174
+
175
+ .pagination-info {
176
+ flex: 1;
177
+ }
178
+
179
+ .pagination-text {
180
+ font-size: 0.875rem;
181
+ color: #6c757d;
182
+ }
183
+
184
+ .pagination-controls {
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 0.5rem;
188
+ }
189
+
190
+ .pagination-pages {
191
+ display: flex;
192
+ align-items: center;
193
+ gap: 0.25rem;
194
+ }
195
+
196
+ .pagination-btn {
197
+ display: flex;
198
+ align-items: center;
199
+ justify-content: center;
200
+ min-width: 2.5rem;
201
+ height: 2.5rem;
202
+ padding: 0.5rem;
203
+ border: 1px solid #dee2e6;
204
+ background: #fff;
205
+ color: #495057;
206
+ text-decoration: none;
207
+ border-radius: 4px;
208
+ cursor: pointer;
209
+ transition: all 0.2s ease;
210
+ font-size: 0.875rem;
211
+ }
212
+
213
+ .pagination-btn:hover:not(:disabled) {
214
+ background: #e9ecef;
215
+ border-color: #adb5bd;
216
+ color: #495057;
217
+ }
218
+
219
+ .pagination-btn.active {
220
+ background: #c6d0d7;
221
+ color: #333;
222
+ }
223
+
224
+ .pagination-btn:disabled {
225
+ opacity: 0.5;
226
+ cursor: not-allowed;
227
+ }
228
+
229
+ .pagination-ellipsis {
230
+ padding: 0.5rem;
231
+ color: #6c757d;
232
+ font-size: 0.875rem;
233
+ }
234
+
235
+ .pagination-size {
236
+ display: flex;
237
+ align-items: center;
238
+ gap: 0.5rem;
239
+ font-size: 0.875rem;
240
+ color: #6c757d;
241
+ }
242
+
243
+ .page-size-select {
244
+ padding: 0.25rem 0.5rem;
245
+ border: 1px solid #dee2e6;
246
+ border-radius: 4px;
247
+ background: #fff;
248
+ font-size: 0.875rem;
249
+ }
250
+
251
+ .page-size-select:focus {
252
+ outline: none;
253
+ border-color: #5681af;
254
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
255
+ }
256
+
257
+ /* Responsive design */
258
+ @media (max-width: 768px) {
259
+ .pagination {
260
+ flex-direction: column;
261
+ gap: 1rem;
262
+ align-items: stretch;
263
+ }
264
+
265
+ .pagination-controls {
266
+ justify-content: center;
267
+ }
268
+
269
+ .pagination-size {
270
+ justify-content: center;
271
+ }
272
+ }
273
+ </style>
@@ -0,0 +1,200 @@
1
+ <template>
2
+ <aside :class="{ 'shown': isOpen, 'stuck': isStuck }" class="sidebar">
3
+ <div class = "flex-row">
4
+ <h2>Navigation</h2>
5
+ <div class = "fg1" />
6
+ <button class="stick-toggle" :aria-pressed="isStuck" :title="isStuck ? 'Unstick sidebar' : 'Stick sidebar'" @click="toggleStick">
7
+ <span v-if="isStuck">
8
+ <HugeiconsIcon :icon="Pin02Icon" width = "1em" height = "1em" />
9
+ </span>
10
+ <span v-else>
11
+ <HugeiconsIcon :icon="PinIcon" width = "1em" height = "1em" />
12
+ </span>
13
+ </button>
14
+ </div>
15
+
16
+ <nav class="mainnav">
17
+ <ul class="navigation-links">
18
+ <li v-for="link in navigationLinks" :key="link.id" :title="link.title">
19
+ <router-link :to="link.path" :class="{ active: isActive(link.path) }">
20
+ <HugeiconsIcon :icon="link.icon" />
21
+ <span>{{ link.title }}</span>
22
+ </router-link>
23
+ </li>
24
+ </ul>
25
+
26
+ <ul class="supplemental-links">
27
+ <li v-for="link in supplementalLinks" :key="link.id" :title="link.title">
28
+ <router-link :to="link.path" :class="{ active: isActive(link.path) }">
29
+ <HugeiconsIcon :icon="link.icon" />
30
+ <span>{{ link.title }}</span>
31
+ </router-link>
32
+ </li>
33
+ </ul>
34
+ </nav>
35
+ </aside>
36
+ </template>
37
+
38
+ <script setup>
39
+ import { ref, onMounted, getCurrentInstance } from 'vue'
40
+ import { useRoute } from 'vue-router'
41
+ import { HugeiconsIcon } from '@hugeicons/vue'
42
+ import { DashboardSquare01Icon } from '@hugeicons/core-free-icons'
43
+ import { LeftToRightListDashIcon } from '@hugeicons/core-free-icons'
44
+ import { Wrench01Icon } from '@hugeicons/core-free-icons'
45
+ import { Pin02Icon } from '@hugeicons/core-free-icons'
46
+ import { PinIcon } from '@hugeicons/core-free-icons'
47
+
48
+ const isOpen = ref(false)
49
+ const isStuck = ref(false)
50
+ const navigationLinks = ref([])
51
+
52
+ const supplementalLinks = ref([])
53
+
54
+ const route = useRoute()
55
+
56
+ function toggleStick() {
57
+ isStuck.value = !isStuck.value
58
+ }
59
+
60
+ function stick() {
61
+ isStuck.value = true
62
+ }
63
+
64
+ function unstick() {
65
+ isStuck.value = false
66
+ }
67
+
68
+ function toggle() {
69
+ isOpen.value = !isOpen.value
70
+ isStuck.value = false
71
+ }
72
+
73
+ function open() {
74
+ isOpen.value = true
75
+ }
76
+
77
+ function close() {
78
+ isOpen.value = false
79
+ isStuck.value = false
80
+ }
81
+
82
+ function isActive(path) {
83
+ return route.path === path
84
+ }
85
+
86
+ function addNavigationLink(link) {
87
+ link.icon = DashboardSquare01Icon
88
+
89
+ const existingIndex = navigationLinks.value.findIndex(l => l.id === link.id)
90
+ if (existingIndex >= 0) {
91
+ navigationLinks.value[existingIndex] = { ...link }
92
+ } else {
93
+ navigationLinks.value.push({ ...link })
94
+ }
95
+ }
96
+
97
+ function addSupplementalLink(link) {
98
+ const existingIndex = supplementalLinks.value.findIndex(l => l.id === link.id)
99
+ if (existingIndex >= 0) {
100
+ supplementalLinks.value[existingIndex] = { ...link }
101
+ } else {
102
+ supplementalLinks.value.push({ ...link })
103
+ }
104
+ }
105
+
106
+ function removeNavigationLink(linkId) {
107
+ navigationLinks.value = navigationLinks.value.filter(link => link.id !== linkId)
108
+ }
109
+
110
+ function removeSupplementalLink(linkId) {
111
+ supplementalLinks.value = supplementalLinks.value.filter(link => link.id !== linkId)
112
+ }
113
+
114
+ function clearNavigationLinks() {
115
+ navigationLinks.value = []
116
+ }
117
+
118
+ function clearSupplementalLinks() {
119
+ supplementalLinks.value = []
120
+ }
121
+
122
+ function getNavigationLinks() {
123
+ return [...navigationLinks.value]
124
+ }
125
+
126
+ function getSupplementalLinks() {
127
+ return [...supplementalLinks.value]
128
+ }
129
+
130
+ defineExpose({
131
+ isOpen,
132
+ navigationLinks,
133
+ supplementalLinks,
134
+ stick,
135
+ unstick,
136
+ toggleStick,
137
+ toggle,
138
+ open,
139
+ close,
140
+ isActive,
141
+ addNavigationLink,
142
+ addSupplementalLink,
143
+ removeNavigationLink,
144
+ removeSupplementalLink,
145
+ clearNavigationLinks,
146
+ clearSupplementalLinks,
147
+ getNavigationLinks,
148
+ getSupplementalLinks
149
+ })
150
+ </script>
151
+
152
+ <style scoped>
153
+
154
+ .active {
155
+ text-decoration: underline;
156
+ }
157
+
158
+ li {
159
+ margin: 0;
160
+ padding: 0;
161
+ }
162
+ .navigation-links a,
163
+ .supplemental-links a {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 0.75rem;
167
+ padding: 0.75rem 1rem;
168
+ color: #333;
169
+ transition: background-color 0.2s ease;
170
+ border-left: 3px solid transparent;
171
+ }
172
+
173
+ .navigation-links a:hover,
174
+ .supplemental-links a:hover {
175
+ background: #f8f9fa;
176
+ color: #007bff;
177
+ }
178
+
179
+ .icon {
180
+ font-size: 1.2em;
181
+ width: 1.5rem;
182
+ text-align: center;
183
+ }
184
+
185
+ .supplemental-links {
186
+ border-top: 1px solid #eee;
187
+ margin-top: 1rem;
188
+ }
189
+
190
+ @media (max-width: 768px) {
191
+ .sidebar {
192
+ width: 100%;
193
+ left: -100%;
194
+ }
195
+
196
+ .sidebar.shown {
197
+ left: 0;
198
+ }
199
+ }
200
+ </style>
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <Header />
3
+
4
+ <div id = "layout">
5
+ <Sidebar ref = "sidebar" />
6
+
7
+ <main>
8
+ <div id = "content">
9
+ <router-view />
10
+
11
+ <footer>
12
+ <span>PicoCrank</span>
13
+ </footer>
14
+ </div>
15
+ </main>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup>
20
+ import { ref } from 'vue';
21
+ import { onMounted } from 'vue';
22
+
23
+ const sidebar = ref(null);
24
+
25
+ onMounted(() => {
26
+ sidebar.value.addNavigationLink({
27
+ title: "Welcome",
28
+ path: "/",
29
+ })
30
+ sidebar.value.open();
31
+ sidebar.value.stick();
32
+ });
33
+
34
+ </script>
@@ -0,0 +1,16 @@
1
+ <template>
2
+ <section class = "with-header-and-content">
3
+ <div class = "section-header">
4
+ Welcome
5
+ </div>
6
+ <div class = "section-content">
7
+ <p>
8
+ Welcome to Femtocrank, a web-based tool for analyzing and visualizing the performance of your code.
9
+ To get started, you can either upload a file or paste your code directly into the editor.
10
+ </p>
11
+ <p>
12
+ For more information on how to use Femtocrank, please refer to the <a href = "/docs">documentation</a>.
13
+ </p>
14
+ </div>
15
+ </section>
16
+ </template>