A place for the small utilities a technician reaches for at a bench. The plugin owns no API and no tables: every tool runs entirely in the browser, so an air-gapped site gets them for free and a bad network cannot break them. Adding the next tool is a view, a route, and one entry in tools.js. First tool is a barcode/QR generator. Content is typed text, a URL, or a CSV (content,label,copies - quoted fields and an optional header both handled), so a batch of a few hundred is one paste. Label stock is adjustable in inches with five presets, and the code renders to an SVG data URI rather than a PNG: a bitmap gets downscaled to label size and smears the module edges a scanner reads, where SVG rasterizes at the printer's resolution with hard edges. It also carries the dot-grid rule that is easy to get wrong by eye. A thermal head cannot render a fraction of a dot, so a code sized off the grid gets uneven modules; pick a DPI and the page says what the current size lands on and what to use instead. The quiet zone is blank label rather than white baked into the code, so it can be tuned - and it applies to CODE128 too, which needs clear space at each end and was letting bars run into the caption. Tech Tools is the first bundled plugin that owns no schema, which two guards did not model: it belongs in the universal installer profile, and upgrade-all reports it 'no-migrations' where every plugin was assumed to report 'ok'. The migration test now asserts that status explicitly for schema-less plugins, so a table-owning plugin whose chain went missing still fails.
94 lines
1.8 KiB
Vue
94 lines
1.8 KiB
Vue
<template>
|
|
<div>
|
|
<div class="page-header">
|
|
<h1>Tech Tools</h1>
|
|
</div>
|
|
|
|
<div class="filters">
|
|
<input
|
|
v-model="search"
|
|
type="text"
|
|
class="form-control"
|
|
placeholder="Search tools..."
|
|
/>
|
|
</div>
|
|
|
|
<div v-for="group in groups" :key="group.category" class="tool-group">
|
|
<h2 class="group-title">{{ group.label }}</h2>
|
|
<div class="tools-grid">
|
|
<router-link
|
|
v-for="tool in group.tools"
|
|
:key="tool.id"
|
|
:to="tool.route"
|
|
class="tool-card card"
|
|
>
|
|
<h3>{{ tool.name }}</h3>
|
|
<p>{{ tool.description }}</p>
|
|
<span class="badge">{{ group.label }}</span>
|
|
</router-link>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!groups.length" class="empty-state">
|
|
No tools match your search.
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { groupTools } from '../tools'
|
|
|
|
const search = ref('')
|
|
const groups = computed(() => groupTools(search.value))
|
|
</script>
|
|
|
|
<style scoped>
|
|
.tool-group {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.group-title {
|
|
font-size: 1.1rem;
|
|
color: var(--text-light);
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.tools-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.tool-card {
|
|
padding: 1.25rem;
|
|
cursor: pointer;
|
|
display: block;
|
|
color: inherit;
|
|
text-decoration: none;
|
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.tool-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
}
|
|
|
|
.tool-card h3 {
|
|
margin: 0 0 0.4rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.tool-card p {
|
|
margin: 0 0 0.75rem;
|
|
color: var(--text-light);
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 2rem;
|
|
color: var(--text-light);
|
|
}
|
|
</style>
|