Skip to content

Commit 851a626

Browse files
authored
Merge pull request #5 from Daschi1/development
v1.4.1
2 parents fca6089 + c44a9e4 commit 851a626

3 files changed

Lines changed: 182 additions & 23 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "cron-builder-parser",
33
"private": true,
4-
"version": "1.4.0",
4+
"version": "1.4.1",
55
"description": "Strict POSIX cron: Builder & Parser",
66
"author": "Daschi (https://github.com/Daschi1)",
77
"repository": {

src/lib/ui/CopyButton.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,5 +40,10 @@
4040
{label}
4141
</Button>
4242
{#if status}
43-
<span class="ml-2 text-xs text-emerald-400" role="status" aria-live="polite">{status}</span>
43+
<span
44+
class={`ml-2 text-xs ${status === "Copied" ? "text-emerald-400" : "text-rose-400"}`}
45+
role="status"
46+
aria-live="polite"
47+
aria-atomic="true">{status}</span
48+
>
4449
{/if}

src/lib/ui/FieldChips.svelte

Lines changed: 175 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script lang="ts">
22
import type { FieldSpec } from "$lib/utils/cron";
33
import { toggleValue, setEvery } from "$lib/utils/cron";
4+
import { onMount, tick } from "svelte";
45
56
let {
67
title,
@@ -16,6 +17,136 @@
1617
labels?: string[] | null;
1718
}>();
1819
20+
type Item = {
21+
key: string;
22+
type: "every" | "value";
23+
value?: number;
24+
text: string; // visible text
25+
title: string; // tooltip
26+
};
27+
28+
let containerEl: HTMLDivElement | null = null;
29+
let measureEl: HTMLDivElement | null = null;
30+
let rows: number[][] = $state([]);
31+
let items: Item[] = $state([]);
32+
let widths: number[] = $state([]);
33+
let containerWidth = $state(0);
34+
35+
const GAP_PX = 8; // Tailwind gap-2
36+
37+
function buildItems(): Item[] {
38+
const out: Item[] = [];
39+
out.push({
40+
key: "every",
41+
type: "every",
42+
text: "Every *",
43+
title: "Every *"
44+
});
45+
for (let v = min; v <= max; v++) {
46+
const label = labels && labels[v - min] ? `${labels[v - min]} (${v})` : String(v);
47+
out.push({
48+
key: `v-${v}`,
49+
type: "value",
50+
value: v,
51+
text: label,
52+
title: labels && labels[v - min] ? `${labels[v - min]} (${v})` : `${v}`
53+
});
54+
}
55+
return out;
56+
}
57+
58+
async function measure() {
59+
if (!measureEl) return;
60+
// Ensure DOM is updated
61+
await tick();
62+
const btns = Array.from(measureEl.querySelectorAll("button"));
63+
widths = btns.map((b) => Math.ceil(b.getBoundingClientRect().width));
64+
}
65+
66+
function packRows() {
67+
if (!containerWidth || widths.length !== items.length) {
68+
rows = [items.map((_, i) => i)];
69+
return;
70+
}
71+
const maxWidth = containerWidth;
72+
const out: number[][] = [];
73+
let i = 0;
74+
let prevRowWidth = Number.POSITIVE_INFINITY;
75+
while (i < items.length) {
76+
let row: number[] = [];
77+
let used = 0;
78+
const limit = Math.min(maxWidth, prevRowWidth);
79+
while (i < items.length) {
80+
const w = widths[i];
81+
const nextUsed = row.length === 0 ? w : used + GAP_PX + w;
82+
if (nextUsed <= limit) {
83+
row.push(i);
84+
used = nextUsed;
85+
i++;
86+
} else {
87+
break;
88+
}
89+
}
90+
if (row.length === 0) {
91+
// Fallback to place one item to avoid infinite loop
92+
row.push(i);
93+
used = widths[i] || 0;
94+
i++;
95+
}
96+
out.push(row);
97+
prevRowWidth = used;
98+
}
99+
rows = out;
100+
}
101+
102+
function recalc() {
103+
items = buildItems();
104+
}
105+
106+
let ro: ResizeObserver | null = null;
107+
onMount(() => {
108+
recalc();
109+
// Observe container for width changes (including responsive font-size changes)
110+
ro = new ResizeObserver((entries) => {
111+
for (const entry of entries) {
112+
containerWidth = Math.floor(entry.contentRect.width);
113+
// Re-measure because media queries may alter widths
114+
measure().then(packRows);
115+
}
116+
});
117+
if (containerEl) ro.observe(containerEl);
118+
// Initial measure/pack
119+
measure().then(packRows);
120+
return () => {
121+
if (ro && containerEl) ro.unobserve(containerEl);
122+
ro = null;
123+
};
124+
});
125+
126+
// Recompute items and re-measure when props change
127+
$effect(() => {
128+
// Depend on labels, min, max
129+
const _l = labels ? labels.join("|") : "";
130+
const _min = min;
131+
const _max = max;
132+
void _l;
133+
void _min;
134+
void _max;
135+
recalc();
136+
// After items update, measure and pack
137+
measure().then(packRows);
138+
});
139+
140+
// Repack when selection may visually affect layout (unlikely, but safe)
141+
$effect(() => {
142+
const any = spec.any;
143+
const count = spec.values.length;
144+
void any;
145+
void count;
146+
// No need to rebuild items; just pack in case slight width changes occur
147+
measure().then(packRows);
148+
});
149+
19150
function onEvery() {
20151
spec = setEvery(spec, min, max);
21152
}
@@ -27,27 +158,50 @@
27158

28159
<div class="space-y-2">
29160
<div class="text-sm font-semibold text-slate-300">{title}</div>
30-
<div class="flex flex-wrap gap-2">
31-
<button
32-
type="button"
33-
class="chip cursor-pointer rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm font-medium transition-colors hover:bg-neutral-800 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950 focus-visible:outline-none aria-pressed:border-emerald-500 aria-pressed:bg-emerald-900/40 aria-pressed:text-emerald-200 md:px-2.5 md:py-1.5 md:text-xs"
34-
aria-pressed={spec.any}
35-
onclick={onEvery}
36-
>Every *
37-
</button>
38-
{#each Array.from({ length: max - min + 1 }, (_, i) => min + i) as v (v)}
39-
<button
40-
type="button"
41-
class="chip cursor-pointer rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm font-medium transition-colors hover:bg-neutral-800 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950 focus-visible:outline-none aria-pressed:border-emerald-500 aria-pressed:bg-emerald-900/40 aria-pressed:text-emerald-200 md:px-2.5 md:py-1.5 md:text-xs"
42-
aria-pressed={!spec.any && spec.values.includes(v)}
43-
onclick={() => onToggle(v)}
44-
>
45-
{#if labels && labels[v - min]}
46-
{labels[v - min]} ({v})
47-
{:else}
48-
{v}
49-
{/if}
50-
</button>
161+
162+
<!-- Measurement container (offscreen, invisible) -->
163+
<div class="invisible absolute top-0 left-0 -z-50">
164+
<div class="flex gap-2" bind:this={measureEl} aria-hidden="true">
165+
{#each items as it (it.key)}
166+
<button
167+
type="button"
168+
class="chip cursor-pointer rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-sm font-medium md:px-2.5 md:py-1.5 md:text-xs"
169+
>
170+
{it.text}
171+
</button>
172+
{/each}
173+
</div>
174+
</div>
175+
176+
<!-- Visible rows container -->
177+
<div class="flex w-full flex-col items-start gap-2" bind:this={containerEl}>
178+
{#each rows as row, rIdx (rIdx)}
179+
<div class="flex gap-2">
180+
{#each row as idx (idx)}
181+
{#if items[idx].type === "every"}
182+
<button
183+
type="button"
184+
class="chip cursor-pointer rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-center text-sm font-medium transition-colors hover:bg-neutral-800 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950 focus-visible:outline-none aria-pressed:border-emerald-500 aria-pressed:bg-emerald-900/40 aria-pressed:text-emerald-200 md:px-2.5 md:py-1.5 md:text-xs"
185+
aria-pressed={spec.any}
186+
onclick={onEvery}
187+
title={items[idx].title}
188+
>{items[idx].text}
189+
</button>
190+
{:else}
191+
<button
192+
type="button"
193+
class="chip cursor-pointer rounded-md border border-neutral-800 bg-neutral-900 px-3 py-2 text-center text-sm font-medium transition-colors hover:bg-neutral-800 hover:brightness-110 focus-visible:ring-2 focus-visible:ring-emerald-500 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-950 focus-visible:outline-none aria-pressed:border-emerald-500 aria-pressed:bg-emerald-900/40 aria-pressed:text-emerald-200 md:px-2.5 md:py-1.5 md:text-xs"
194+
aria-pressed={!spec.any &&
195+
items[idx].value !== undefined &&
196+
spec.values.includes(items[idx].value)}
197+
onclick={() => items[idx].value !== undefined && onToggle(items[idx].value)}
198+
title={items[idx].title}
199+
>
200+
{items[idx].text}
201+
</button>
202+
{/if}
203+
{/each}
204+
</div>
51205
{/each}
52206
</div>
53207
</div>

0 commit comments

Comments
 (0)