markgraphy 0.1.1

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.
Files changed (166) hide show
  1. package/LICENSE.txt +21 -0
  2. package/README.md +154 -0
  3. package/dist/animated/GraphAgni.svelte +430 -0
  4. package/dist/animated/GraphAgni.svelte.d.ts +22 -0
  5. package/dist/animated/GraphAum.svelte +294 -0
  6. package/dist/animated/GraphAum.svelte.d.ts +20 -0
  7. package/dist/animated/GraphBoot.svelte +325 -0
  8. package/dist/animated/GraphBoot.svelte.d.ts +28 -0
  9. package/dist/animated/GraphCron.svelte +476 -0
  10. package/dist/animated/GraphCron.svelte.d.ts +42 -0
  11. package/dist/animated/GraphDamru.svelte +341 -0
  12. package/dist/animated/GraphDamru.svelte.d.ts +18 -0
  13. package/dist/animated/GraphDeps.svelte +245 -0
  14. package/dist/animated/GraphDeps.svelte.d.ts +29 -0
  15. package/dist/animated/GraphDiya.svelte +330 -0
  16. package/dist/animated/GraphDiya.svelte.d.ts +22 -0
  17. package/dist/animated/GraphFire.svelte +246 -0
  18. package/dist/animated/GraphFire.svelte.d.ts +22 -0
  19. package/dist/animated/GraphFlame.svelte +258 -0
  20. package/dist/animated/GraphFlame.svelte.d.ts +14 -0
  21. package/dist/animated/GraphFlowPlayer.svelte +346 -0
  22. package/dist/animated/GraphFlowPlayer.svelte.d.ts +27 -0
  23. package/dist/animated/GraphGanga.svelte +305 -0
  24. package/dist/animated/GraphGanga.svelte.d.ts +20 -0
  25. package/dist/animated/GraphHash.svelte +251 -0
  26. package/dist/animated/GraphHash.svelte.d.ts +20 -0
  27. package/dist/animated/GraphJapa.svelte +253 -0
  28. package/dist/animated/GraphJapa.svelte.d.ts +20 -0
  29. package/dist/animated/GraphLife.svelte +312 -0
  30. package/dist/animated/GraphLife.svelte.d.ts +27 -0
  31. package/dist/animated/GraphMandala.svelte +262 -0
  32. package/dist/animated/GraphMandala.svelte.d.ts +24 -0
  33. package/dist/animated/GraphMandel.svelte +290 -0
  34. package/dist/animated/GraphMandel.svelte.d.ts +27 -0
  35. package/dist/animated/GraphPulse.svelte +248 -0
  36. package/dist/animated/GraphPulse.svelte.d.ts +24 -0
  37. package/dist/animated/GraphRain.svelte +280 -0
  38. package/dist/animated/GraphRain.svelte.d.ts +22 -0
  39. package/dist/animated/GraphScatter.svelte +395 -0
  40. package/dist/animated/GraphScatter.svelte.d.ts +31 -0
  41. package/dist/animated/GraphScope.svelte +197 -0
  42. package/dist/animated/GraphScope.svelte.d.ts +22 -0
  43. package/dist/animated/GraphSequence.svelte +247 -0
  44. package/dist/animated/GraphSequence.svelte.d.ts +18 -0
  45. package/dist/animated/GraphSpinners.svelte +109 -0
  46. package/dist/animated/GraphSpinners.svelte.d.ts +22 -0
  47. package/dist/animated/GraphState.svelte +270 -0
  48. package/dist/animated/GraphState.svelte.d.ts +14 -0
  49. package/dist/animated/GraphStream.svelte +223 -0
  50. package/dist/animated/GraphStream.svelte.d.ts +24 -0
  51. package/dist/animated/GraphSurya.svelte +268 -0
  52. package/dist/animated/GraphSurya.svelte.d.ts +22 -0
  53. package/dist/animated/GraphTerminal.svelte +238 -0
  54. package/dist/animated/GraphTerminal.svelte.d.ts +28 -0
  55. package/dist/animated/GraphTicker.svelte +163 -0
  56. package/dist/animated/GraphTicker.svelte.d.ts +21 -0
  57. package/dist/animated/GraphTypewriter.svelte +130 -0
  58. package/dist/animated/GraphTypewriter.svelte.d.ts +22 -0
  59. package/dist/animated/GraphWorkflow.svelte +570 -0
  60. package/dist/animated/GraphWorkflow.svelte.d.ts +29 -0
  61. package/dist/assets/favicon.svg +21 -0
  62. package/dist/diagram/AmplifierDiagram.svelte +206 -0
  63. package/dist/diagram/AmplifierDiagram.svelte.d.ts +12 -0
  64. package/dist/diagram/AsciiDiagram.svelte +212 -0
  65. package/dist/diagram/AsciiDiagram.svelte.d.ts +24 -0
  66. package/dist/diagram/DiagramEditor.svelte +414 -0
  67. package/dist/diagram/DiagramEditor.svelte.d.ts +3 -0
  68. package/dist/diagram/MetricsTableDiagram.svelte +125 -0
  69. package/dist/diagram/MetricsTableDiagram.svelte.d.ts +18 -0
  70. package/dist/diagram/NestedRadiiDiagram.svelte +218 -0
  71. package/dist/diagram/NestedRadiiDiagram.svelte.d.ts +12 -0
  72. package/dist/diagram/PromptLoopDiagram.svelte +331 -0
  73. package/dist/diagram/PromptLoopDiagram.svelte.d.ts +12 -0
  74. package/dist/diagram/ascii.d.ts +35 -0
  75. package/dist/diagram/ascii.js +83 -0
  76. package/dist/diagram/parser.d.ts +16 -0
  77. package/dist/diagram/parser.js +71 -0
  78. package/dist/frame/Graph.svelte +141 -0
  79. package/dist/frame/Graph.svelte.d.ts +14 -0
  80. package/dist/frame/GraphArrow.svelte +49 -0
  81. package/dist/frame/GraphArrow.svelte.d.ts +9 -0
  82. package/dist/frame/GraphBody.svelte +39 -0
  83. package/dist/frame/GraphBody.svelte.d.ts +10 -0
  84. package/dist/frame/GraphRule.svelte +21 -0
  85. package/dist/frame/GraphRule.svelte.d.ts +6 -0
  86. package/dist/frame/GraphTick.svelte +23 -0
  87. package/dist/frame/GraphTick.svelte.d.ts +9 -0
  88. package/dist/frame/GraphTrack.svelte +29 -0
  89. package/dist/frame/GraphTrack.svelte.d.ts +14 -0
  90. package/dist/frame/clock.d.ts +10 -0
  91. package/dist/frame/clock.js +62 -0
  92. package/dist/frame/glyphs.d.ts +22 -0
  93. package/dist/frame/glyphs.js +53 -0
  94. package/dist/frame/motion.css +27 -0
  95. package/dist/frame/motion.d.ts +24 -0
  96. package/dist/frame/motion.js +47 -0
  97. package/dist/frame/tone.d.ts +11 -0
  98. package/dist/frame/tone.js +64 -0
  99. package/dist/graphs/GraphActivity.svelte +387 -0
  100. package/dist/graphs/GraphActivity.svelte.d.ts +22 -0
  101. package/dist/graphs/GraphBars.svelte +194 -0
  102. package/dist/graphs/GraphBars.svelte.d.ts +20 -0
  103. package/dist/graphs/GraphBullet.svelte +208 -0
  104. package/dist/graphs/GraphBullet.svelte.d.ts +22 -0
  105. package/dist/graphs/GraphCalendar.svelte +203 -0
  106. package/dist/graphs/GraphCalendar.svelte.d.ts +20 -0
  107. package/dist/graphs/GraphCells.svelte +181 -0
  108. package/dist/graphs/GraphCells.svelte.d.ts +18 -0
  109. package/dist/graphs/GraphCompare.svelte +192 -0
  110. package/dist/graphs/GraphCompare.svelte.d.ts +18 -0
  111. package/dist/graphs/GraphCountdown.svelte +96 -0
  112. package/dist/graphs/GraphCountdown.svelte.d.ts +14 -0
  113. package/dist/graphs/GraphDiff.svelte +181 -0
  114. package/dist/graphs/GraphDiff.svelte.d.ts +18 -0
  115. package/dist/graphs/GraphFlow.svelte +137 -0
  116. package/dist/graphs/GraphFlow.svelte.d.ts +21 -0
  117. package/dist/graphs/GraphFunnel.svelte +190 -0
  118. package/dist/graphs/GraphFunnel.svelte.d.ts +21 -0
  119. package/dist/graphs/GraphGantt.svelte +258 -0
  120. package/dist/graphs/GraphGantt.svelte.d.ts +25 -0
  121. package/dist/graphs/GraphHeatmap.svelte +287 -0
  122. package/dist/graphs/GraphHeatmap.svelte.d.ts +22 -0
  123. package/dist/graphs/GraphInvoice.svelte +294 -0
  124. package/dist/graphs/GraphInvoice.svelte.d.ts +34 -0
  125. package/dist/graphs/GraphKpi.svelte +166 -0
  126. package/dist/graphs/GraphKpi.svelte.d.ts +17 -0
  127. package/dist/graphs/GraphLatency.svelte +340 -0
  128. package/dist/graphs/GraphLatency.svelte.d.ts +28 -0
  129. package/dist/graphs/GraphMeter.svelte +121 -0
  130. package/dist/graphs/GraphMeter.svelte.d.ts +16 -0
  131. package/dist/graphs/GraphPlot.svelte +226 -0
  132. package/dist/graphs/GraphPlot.svelte.d.ts +18 -0
  133. package/dist/graphs/GraphQuota.svelte +303 -0
  134. package/dist/graphs/GraphQuota.svelte.d.ts +27 -0
  135. package/dist/graphs/GraphRank.svelte +141 -0
  136. package/dist/graphs/GraphRank.svelte.d.ts +21 -0
  137. package/dist/graphs/GraphSlo.svelte +325 -0
  138. package/dist/graphs/GraphSlo.svelte.d.ts +31 -0
  139. package/dist/graphs/GraphSlope.svelte +197 -0
  140. package/dist/graphs/GraphSlope.svelte.d.ts +19 -0
  141. package/dist/graphs/GraphSpark.svelte +125 -0
  142. package/dist/graphs/GraphSpark.svelte.d.ts +15 -0
  143. package/dist/graphs/GraphSpec.svelte +69 -0
  144. package/dist/graphs/GraphSpec.svelte.d.ts +14 -0
  145. package/dist/graphs/GraphStack.svelte +266 -0
  146. package/dist/graphs/GraphStack.svelte.d.ts +25 -0
  147. package/dist/graphs/GraphStat.svelte +103 -0
  148. package/dist/graphs/GraphStat.svelte.d.ts +16 -0
  149. package/dist/graphs/GraphTable.svelte +133 -0
  150. package/dist/graphs/GraphTable.svelte.d.ts +15 -0
  151. package/dist/graphs/GraphTimeline.svelte +164 -0
  152. package/dist/graphs/GraphTimeline.svelte.d.ts +18 -0
  153. package/dist/graphs/GraphTimer.svelte +117 -0
  154. package/dist/graphs/GraphTimer.svelte.d.ts +15 -0
  155. package/dist/graphs/GraphTree.svelte +147 -0
  156. package/dist/graphs/GraphTree.svelte.d.ts +16 -0
  157. package/dist/graphs/GraphUptime.svelte +210 -0
  158. package/dist/graphs/GraphUptime.svelte.d.ts +18 -0
  159. package/dist/graphs/GraphWaffle.svelte +131 -0
  160. package/dist/graphs/GraphWaffle.svelte.d.ts +17 -0
  161. package/dist/graphs/GraphWaterfall.svelte +258 -0
  162. package/dist/graphs/GraphWaterfall.svelte.d.ts +22 -0
  163. package/dist/index.d.ts +147 -0
  164. package/dist/index.js +87 -0
  165. package/dist/themes.css +54 -0
  166. package/package.json +67 -0
@@ -0,0 +1,476 @@
1
+ <script module lang="ts">
2
+ export interface GraphCronProps {
3
+ /** Caption drawn on the top edge as `[ TITLE ]`. */
4
+ title: string;
5
+ /** Standard 5-field cron expression: `min hour dom mon dow`. */
6
+ expr: string;
7
+ /** Number of next runs to render on the tick timeline. Default 8. */
8
+ count?: number;
9
+ /** Base time as ms since epoch; next runs are computed from this point. Default now. */
10
+ baseTime?: number;
11
+ /** Scene width in characters. Default 60. */
12
+ cols?: number;
13
+ /** Scene height in rows. Default 12. */
14
+ rows?: number;
15
+ /** Animate the tick highlight. Default true. Reduced motion freezes on first run. */
16
+ animated?: boolean;
17
+ /** Milliseconds per tick. Default 220. */
18
+ speedMs?: number;
19
+ /** Caption rendered muted under the timeline. */
20
+ label?: string;
21
+ class?: string;
22
+ }
23
+
24
+ interface ParsedCron {
25
+ minutes: Set<number>;
26
+ hours: Set<number>;
27
+ doms: Set<number>;
28
+ months: Set<number>;
29
+ dows: Set<number>;
30
+ /** True when both DOM and DOW are restricted — the OR-match rule applies. */
31
+ bothRestricted: boolean;
32
+ }
33
+
34
+ interface RunRow {
35
+ ms: number;
36
+ hourMin: string;
37
+ date: string;
38
+ }
39
+
40
+ interface Seg {
41
+ text: string;
42
+ cls?: string;
43
+ }
44
+
45
+ const MONTH_NAMES: Record<string, number> = {
46
+ jan: 1,
47
+ feb: 2,
48
+ mar: 3,
49
+ apr: 4,
50
+ may: 5,
51
+ jun: 6,
52
+ jul: 7,
53
+ aug: 8,
54
+ sep: 9,
55
+ oct: 10,
56
+ nov: 11,
57
+ dec: 12
58
+ };
59
+ const DOW_NAMES: Record<string, number> = {
60
+ sun: 0,
61
+ mon: 1,
62
+ tue: 2,
63
+ wed: 3,
64
+ thu: 4,
65
+ fri: 5,
66
+ sat: 6
67
+ };
68
+
69
+ /** Parse a single cron field into a set of allowed integer values. */
70
+ function parseField(
71
+ raw: string,
72
+ min: number,
73
+ max: number,
74
+ names?: Record<string, number>
75
+ ): Set<number> {
76
+ const out = new Set<number>();
77
+ const parts = raw.split(',');
78
+ for (const partRaw of parts) {
79
+ const part = partRaw.trim().toLowerCase();
80
+ if (part === '' || part === '*') {
81
+ for (let v = min; v <= max; v++) out.add(v);
82
+ continue;
83
+ }
84
+
85
+ const stepSplit = part.split('/');
86
+ const step = stepSplit.length > 1 ? parseInt(stepSplit[1], 10) : 1;
87
+ if (!Number.isFinite(step) || step <= 0) {
88
+ throw new Error(`invalid step in field: ${raw}`);
89
+ }
90
+
91
+ let rangeStr = stepSplit[0];
92
+ let lo = min;
93
+ let hi = max;
94
+ if (rangeStr !== '*' && rangeStr !== '') {
95
+ const rangeParts = rangeStr.split('-');
96
+ const a = rangeParts[0];
97
+ const b = rangeParts[1];
98
+ lo = parseToken(a, min, max, names);
99
+ hi = rangeParts.length > 1 && b !== undefined ? parseToken(b, min, max, names) : lo;
100
+ if (lo > hi) {
101
+ const tmp = lo;
102
+ lo = hi;
103
+ hi = tmp;
104
+ }
105
+ }
106
+ for (let v = lo; v <= hi; v += step) out.add(v);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ function parseToken(token: string, min: number, max: number, names?: Record<string, number>): number {
112
+ const t = token.trim().toLowerCase();
113
+ if (names && names[t] !== undefined) return names[t];
114
+ const n = parseInt(t, 10);
115
+ if (!Number.isFinite(n)) {
116
+ throw new Error(`invalid token: ${token}`);
117
+ }
118
+ return Math.max(min, Math.min(max, n));
119
+ }
120
+
121
+ /** Parse a 5-field cron expression. Throws on invalid input. */
122
+ export function parseCron(expr: string): ParsedCron {
123
+ const trimmed = expr.trim();
124
+ if (!trimmed) throw new Error('empty cron expression');
125
+ const fields = trimmed.split(/\s+/);
126
+ if (fields.length !== 5) {
127
+ throw new Error(`expected 5 fields, got ${fields.length}`);
128
+ }
129
+ const [minF, hourF, domF, monF, dowF] = fields;
130
+ const minutes = parseField(minF, 0, 59);
131
+ const hours = parseField(hourF, 0, 23);
132
+ const doms = parseField(domF, 1, 31);
133
+ const months = parseField(monF, 1, 12, MONTH_NAMES);
134
+ // DOW range is 0-6; treat 7 as 0 (Sunday) like Vixie cron.
135
+ const dows = parseField(dowF, 0, 7, DOW_NAMES);
136
+ if (dows.has(7)) {
137
+ dows.delete(7);
138
+ dows.add(0);
139
+ }
140
+ const domRestricted = domF.trim() !== '*';
141
+ const dowRestricted = dowF.trim() !== '*';
142
+ const bothRestricted = domRestricted && dowRestricted;
143
+ return { minutes, hours, doms, months, dows, bothRestricted };
144
+ }
145
+
146
+ /** True when the given Date matches a parsed cron expression. */
147
+ function matches(parsed: ParsedCron, d: Date): boolean {
148
+ if (!parsed.minutes.has(d.getMinutes())) return false;
149
+ if (!parsed.hours.has(d.getHours())) return false;
150
+ if (!parsed.months.has(d.getMonth() + 1)) return false;
151
+ const day = d.getDate();
152
+ const dow = d.getDay();
153
+ if (parsed.bothRestricted) {
154
+ if (!parsed.doms.has(day) && !parsed.dows.has(dow)) return false;
155
+ } else {
156
+ if (!parsed.doms.has(day)) return false;
157
+ if (!parsed.dows.has(dow)) return false;
158
+ }
159
+ return true;
160
+ }
161
+
162
+ /** Compute the next N run times strictly after `from` (ms). */
163
+ export function nextRuns(expr: string, from: number, count: number): RunRow[] {
164
+ const parsed = parseCron(expr);
165
+ const out: RunRow[] = [];
166
+ // Start one minute after `from` so we get strictly later runs.
167
+ const cursor = new Date(from);
168
+ cursor.setSeconds(0, 0);
169
+ cursor.setMinutes(cursor.getMinutes() + 1);
170
+ // Cap the search to four years of minutes to avoid runaway loops.
171
+ const maxIter = 4 * 366 * 24 * 60;
172
+ for (let i = 0; i < maxIter && out.length < count; i++) {
173
+ if (matches(parsed, cursor)) {
174
+ out.push(formatRun(cursor));
175
+ cursor.setMinutes(cursor.getMinutes() + 1);
176
+ continue;
177
+ }
178
+ cursor.setMinutes(cursor.getMinutes() + 1);
179
+ }
180
+ return out;
181
+ }
182
+
183
+ function pad2(n: number): string {
184
+ return n < 10 ? `0${n}` : String(n);
185
+ }
186
+
187
+ function formatRun(d: Date): RunRow {
188
+ const hourMin = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
189
+ const month = pad2(d.getMonth() + 1);
190
+ const day = pad2(d.getDate());
191
+ return { ms: d.getTime(), hourMin, date: `${d.getFullYear()}-${month}-${day}` };
192
+ }
193
+
194
+ /** Run-length encode a row of run markers into classed segments. */
195
+ function tickSegs(
196
+ count: number,
197
+ active: number,
198
+ cols: number,
199
+ nowCol: number,
200
+ showNow: boolean
201
+ ): Seg[] {
202
+ if (count <= 0 || cols <= 0) return [];
203
+ const segs: Seg[] = [];
204
+ // Build a string of `cols` characters where each run is a marker.
205
+ const char = (idx: number): { ch: string; cls: string | undefined } => {
206
+ // Map run idx to a plot column.
207
+ const col = Math.round((idx / Math.max(1, count - 1)) * (cols - 1));
208
+ if (showNow && col === nowCol) return { ch: '|', cls: 'now' };
209
+ if (idx === active) return { ch: '●', cls: 'hot' };
210
+ return { ch: '·', cls: 'faint' };
211
+ };
212
+
213
+ let buf = '';
214
+ let cls: string | undefined;
215
+ const flush = (next: string | undefined) => {
216
+ if (next === cls) {
217
+ buf += next === undefined ? ' ' : char(0).ch;
218
+ } else {
219
+ if (buf.length > 0) segs.push({ text: buf, cls });
220
+ cls = next;
221
+ buf = ' ';
222
+ }
223
+ };
224
+ void flush;
225
+
226
+ for (let i = 0; i < cols; i++) {
227
+ // Which run (if any) lands on this column?
228
+ let runAt = -1;
229
+ for (let r = 0; r < count; r++) {
230
+ if (Math.round((r / Math.max(1, count - 1)) * (cols - 1)) === i) {
231
+ runAt = r;
232
+ break;
233
+ }
234
+ }
235
+ let next: { ch: string; cls: string | undefined };
236
+ if (showNow && i === nowCol) next = { ch: '|', cls: 'now' };
237
+ else if (runAt >= 0 && runAt === active) next = { ch: '●', cls: 'hot' };
238
+ else if (runAt >= 0) next = { ch: '·', cls: 'faint' };
239
+ else next = { ch: ' ', cls: undefined };
240
+
241
+ if (next.cls === cls) {
242
+ buf += next.ch;
243
+ } else {
244
+ if (buf.length > 0) segs.push({ text: buf, cls });
245
+ cls = next.cls;
246
+ buf = next.ch;
247
+ }
248
+ }
249
+ if (buf.length > 0) segs.push({ text: buf, cls });
250
+ return segs;
251
+ }
252
+ </script>
253
+
254
+ <script lang="ts">
255
+ import Graph from '../frame/Graph.svelte';
256
+ import GraphBody from '../frame/GraphBody.svelte';
257
+
258
+ let {
259
+ title,
260
+ expr,
261
+ count = 8,
262
+ baseTime,
263
+ cols = 60,
264
+ rows = 12,
265
+ animated = true,
266
+ speedMs = 220,
267
+ label = '',
268
+ class: className = ''
269
+ }: GraphCronProps = $props();
270
+
271
+ // svelte-ignore state_referenced_locally
272
+ const width = Math.max(20, cols);
273
+ // svelte-ignore state_referenced_locally
274
+ const runCount = Math.max(1, Math.min(64, count));
275
+
276
+ const moving = $derived(
277
+ animated &&
278
+ (typeof window === 'undefined' ||
279
+ !window.matchMedia('(prefers-reduced-motion: reduce)').matches)
280
+ );
281
+
282
+ const parsed = $derived.by(() => {
283
+ try {
284
+ return { ok: true as const, value: parseCron(expr) };
285
+ } catch (err) {
286
+ return { ok: false as const, error: err instanceof Error ? err.message : String(err) };
287
+ }
288
+ });
289
+
290
+ const runs = $derived.by((): RunRow[] => {
291
+ if (!parsed.ok) return [];
292
+ const base = baseTime ?? Date.now();
293
+ try {
294
+ return nextRuns(expr, base, runCount);
295
+ } catch {
296
+ return [];
297
+ }
298
+ });
299
+
300
+ // Tick: when animated, advance which run is "active" on the timeline.
301
+ let tick = $state(0);
302
+ $effect(() => {
303
+ if (!moving || runs.length < 2) {
304
+ tick = 0;
305
+ return;
306
+ }
307
+ const timer = window.setInterval(() => {
308
+ tick = (tick + 1) % runs.length;
309
+ }, Math.max(60, speedMs));
310
+ return () => window.clearInterval(timer);
311
+ });
312
+
313
+ // Frozen state: highlight the first run.
314
+ const active = $derived(moving ? tick : 0);
315
+
316
+ // "Now" indicator column — the cursor that sweeps across.
317
+ const nowCol = $derived.by(() => {
318
+ if (runs.length < 2) return -1;
319
+ const spanMs = runs[runs.length - 1].ms - runs[0].ms;
320
+ if (spanMs <= 0) return 0;
321
+ // Sweep one slot per (spanMs / count) of the way per tick.
322
+ const totalTicks = runs.length;
323
+ const slot = Math.min(totalTicks - 1, Math.floor((tick / totalTicks) * totalTicks));
324
+ return Math.round((slot / Math.max(1, runs.length - 1)) * (width - 1));
325
+ });
326
+
327
+ // Build the row-by-row view: row 0 = cron expression echo,
328
+ // row 1 = the tick timeline, row 2 = date ribbon, row 3 = hour:min labels,
329
+ // row 4 = next/now caption.
330
+ const headerLine = $derived(
331
+ parsed.ok ? `${expr}` : `error: ${parsed.error}`
332
+ );
333
+ const headerPad = $derived(
334
+ ' '.repeat(Math.max(0, Math.floor((width - headerLine.length) / 2)))
335
+ );
336
+ const headerRightPad = $derived(
337
+ ' '.repeat(Math.max(0, width - headerLine.length - headerPad.length))
338
+ );
339
+
340
+ const tickRow = $derived(tickSegs(runs.length, active, width, nowCol, moving));
341
+
342
+ // Date ribbon under the tick line, left-aligned to the markers.
343
+ const dateRow = $derived.by((): Seg[] => {
344
+ if (runs.length === 0) return [{ text: ' '.repeat(width) }];
345
+ const slots = runs.map((r) => r.date);
346
+ const segs: Seg[] = [];
347
+ const cells = width;
348
+ for (let i = 0; i < cells; i++) {
349
+ // Which run (if any) maps to this column?
350
+ let runAt = -1;
351
+ for (let r = 0; r < runs.length; r++) {
352
+ if (Math.round((r / Math.max(1, runs.length - 1)) * (cells - 1)) === i) {
353
+ runAt = r;
354
+ break;
355
+ }
356
+ }
357
+ const ch = runAt >= 0 ? slots[runAt].slice(-2) : ' ';
358
+ const cls = runAt === active ? 'hot' : runAt >= 0 ? 'faint' : undefined;
359
+ if (segs.length === 0 || segs[segs.length - 1].cls !== cls) {
360
+ segs.push({ text: ch, cls });
361
+ } else {
362
+ segs[segs.length - 1] = { text: segs[segs.length - 1].text + ch, cls };
363
+ }
364
+ }
365
+ return segs;
366
+ });
367
+
368
+ // Hour:min ribbon.
369
+ const hourMinRow = $derived.by((): Seg[] => {
370
+ if (runs.length === 0) return [{ text: ' '.repeat(width) }];
371
+ const segs: Seg[] = [];
372
+ for (let i = 0; i < width; i++) {
373
+ let runAt = -1;
374
+ for (let r = 0; r < runs.length; r++) {
375
+ if (Math.round((r / Math.max(1, runs.length - 1)) * (width - 1)) === i) {
376
+ runAt = r;
377
+ break;
378
+ }
379
+ }
380
+ const ch = runAt >= 0 ? runs[runAt].hourMin : ' ';
381
+ const cls = runAt === active ? 'hot' : runAt >= 0 ? 'faint' : undefined;
382
+ if (segs.length === 0 || segs[segs.length - 1].cls !== cls) {
383
+ segs.push({ text: ch, cls });
384
+ } else {
385
+ segs[segs.length - 1] = { text: segs[segs.length - 1].text + ch, cls };
386
+ }
387
+ }
388
+ return segs;
389
+ });
390
+
391
+ // "next: 2026-09-06 12:30" caption.
392
+ const captionText = $derived.by(() => {
393
+ if (runs.length === 0) return 'no runs match this expression';
394
+ const r = runs[active] ?? runs[0];
395
+ return `next: ${r.date} ${r.hourMin}`;
396
+ });
397
+
398
+ const captionRow = $derived.by((): Seg[] => {
399
+ const text = ` ${captionText}`;
400
+ const pad = Math.max(0, width - text.length);
401
+ return [{ text: text + ' '.repeat(pad), cls: 'ax' }];
402
+ });
403
+
404
+ const headerRow = $derived.by((): Seg[] => {
405
+ const text = `${headerPad}${headerLine}${headerRightPad}`;
406
+ return [{ text, cls: parsed.ok ? 'ax' : 'warn' }];
407
+ });
408
+ </script>
409
+
410
+ <Graph {title} class={className}>
411
+ <GraphBody>
412
+ <div class="viewport">
413
+ <pre class="art" aria-hidden="true"><code>{#each [headerRow] as row, r (r)}{#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{'\n'}{/each}{#each [tickRow] as row, r (r)}{#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{'\n'}{/each}{#each [dateRow] as row, r (r)}{#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{'\n'}{/each}{#each [hourMinRow] as row, r (r)}{#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{'\n'}{/each}{#each [captionRow] as row, r (r)}{#each row as seg, j (j)}{#if seg.cls}<span class={seg.cls}>{seg.text}</span>{:else}{seg.text}{/if}{/each}{/each}</code></pre>
414
+ {#if label}
415
+ <p class="caption">{label}</p>
416
+ {/if}
417
+ <span class="sr-only">
418
+ Cron timeline. Expression: {parsed.ok ? expr : `error: ${parsed.error}`}. {runs.length} runs.
419
+ </span>
420
+ </div>
421
+ </GraphBody>
422
+ </Graph>
423
+
424
+ <style>
425
+ .viewport {
426
+ overflow-x: auto;
427
+ }
428
+
429
+ .art {
430
+ margin: 0;
431
+ font-size: 0.78rem;
432
+ line-height: 1.35;
433
+ color: var(--graph-foreground, oklch(0.93 0 0));
434
+ white-space: pre;
435
+ font-variant-numeric: tabular-nums;
436
+ }
437
+
438
+ .hot {
439
+ color: var(--graph-accent, oklch(0.78 0.17 155));
440
+ font-weight: 600;
441
+ }
442
+
443
+ .faint {
444
+ color: var(--graph-faint, oklch(0.3 0 0));
445
+ }
446
+
447
+ .now {
448
+ color: var(--graph-accent-2, oklch(0.78 0.12 70));
449
+ }
450
+
451
+ .ax {
452
+ color: var(--graph-muted, oklch(0.62 0 0));
453
+ }
454
+
455
+ .warn {
456
+ color: oklch(0.7 0.18 35);
457
+ }
458
+
459
+ .caption {
460
+ margin: 0.4rem 0 0;
461
+ font-size: 0.78rem;
462
+ color: var(--graph-muted, oklch(0.62 0 0));
463
+ }
464
+
465
+ .sr-only {
466
+ position: absolute;
467
+ width: 1px;
468
+ height: 1px;
469
+ padding: 0;
470
+ margin: -1px;
471
+ overflow: hidden;
472
+ clip-path: inset(50%);
473
+ white-space: nowrap;
474
+ border-width: 0;
475
+ }
476
+ </style>
@@ -0,0 +1,42 @@
1
+ export interface GraphCronProps {
2
+ /** Caption drawn on the top edge as `[ TITLE ]`. */
3
+ title: string;
4
+ /** Standard 5-field cron expression: `min hour dom mon dow`. */
5
+ expr: string;
6
+ /** Number of next runs to render on the tick timeline. Default 8. */
7
+ count?: number;
8
+ /** Base time as ms since epoch; next runs are computed from this point. Default now. */
9
+ baseTime?: number;
10
+ /** Scene width in characters. Default 60. */
11
+ cols?: number;
12
+ /** Scene height in rows. Default 12. */
13
+ rows?: number;
14
+ /** Animate the tick highlight. Default true. Reduced motion freezes on first run. */
15
+ animated?: boolean;
16
+ /** Milliseconds per tick. Default 220. */
17
+ speedMs?: number;
18
+ /** Caption rendered muted under the timeline. */
19
+ label?: string;
20
+ class?: string;
21
+ }
22
+ interface ParsedCron {
23
+ minutes: Set<number>;
24
+ hours: Set<number>;
25
+ doms: Set<number>;
26
+ months: Set<number>;
27
+ dows: Set<number>;
28
+ /** True when both DOM and DOW are restricted — the OR-match rule applies. */
29
+ bothRestricted: boolean;
30
+ }
31
+ interface RunRow {
32
+ ms: number;
33
+ hourMin: string;
34
+ date: string;
35
+ }
36
+ /** Parse a 5-field cron expression. Throws on invalid input. */
37
+ export declare function parseCron(expr: string): ParsedCron;
38
+ /** Compute the next N run times strictly after `from` (ms). */
39
+ export declare function nextRuns(expr: string, from: number, count: number): RunRow[];
40
+ declare const GraphCron: import("svelte").Component<GraphCronProps, {}, "">;
41
+ type GraphCron = ReturnType<typeof GraphCron>;
42
+ export default GraphCron;