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.
- package/.github/workflows/release.yml +36 -0
- package/README.md +12 -0
- package/index.html +20 -0
- package/logo.png +0 -0
- package/logo.svg +53 -0
- package/package.json +32 -0
- package/router.js +18 -0
- package/vite.config.js +29 -0
- package/vue/components/Breadcrumbs.vue +58 -0
- package/vue/components/Header.vue +8 -0
- package/vue/components/Pagination.vue +273 -0
- package/vue/components/Sidebar.vue +200 -0
- package/vue/views/App.vue +34 -0
- package/vue/views/Welcome.vue +16 -0
|
@@ -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
|
+
[](#none)
|
|
8
|
+
[](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
|
+
»
|
|
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,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>
|