@spaethtech/svelte-ui 0.7.1-dev.35.6496cbd → 0.7.1-dev.38.b73de30

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.
@@ -265,16 +265,45 @@
265
265
  </tbody>
266
266
  </table>
267
267
  </div>
268
+ <div>
269
+ <h4 class="mb-1 font-semibold">Dynamic dates</h4>
270
+ <p class="mb-1 text-xs [color:color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]">
271
+ <code class="font-mono">now()</code> is the current time, resolved when the query runs. Add a
272
+ signed offset <code class="font-mono">now()±&lt;n&gt;&lt;unit&gt;</code> — units
273
+ <code>s</code> <code>m</code> <code>h</code> <code>d</code> <code>w</code>. Great with a date
274
+ column and <code>&gt;=</code>/<code>&lt;</code> or a range.
275
+ </p>
276
+ <table class="w-full text-left">
277
+ <tbody
278
+ class="[&_td]:py-0.5 [&_td:first-child]:whitespace-nowrap [&_td:first-child]:pr-4 [&_td:first-child]:font-mono [&_td:last-child]:text-xs [&_td:last-child]:[color:color-mix(in_srgb,var(--ui-color-text)_65%,transparent)]"
279
+ >
280
+ <tr><td>$created &gt;= now()-7d</td><td>in the last 7 days</td></tr>
281
+ <tr><td>$expires &lt; now()</td><td>already expired</td></tr>
282
+ <tr><td>$due |= [now()..now()+30d]</td><td>due within 30 days</td></tr>
283
+ </tbody>
284
+ </table>
285
+ </div>
268
286
  <div>
269
287
  <h4 class="mb-1 font-semibold">Examples</h4>
270
- <pre
271
- class="overflow-x-auto rounded p-2 font-mono text-xs [background-color:color-mix(in_srgb,var(--ui-color-text)_6%,transparent)]"><code
272
- >$status == active
273
- $email ~= /@gmail\.com$/i
274
- $price |= [20..50) && $role |= [admin, ops]
275
- $ip|inet |= [10.0.0.0|inet..10.255.255.255|inet]
276
- "lemon balm" || $featured == true</code
277
- ></pre>
288
+ <table class="w-full text-left">
289
+ <tbody
290
+ class="[&_td]:py-0.5 [&_td:first-child]:whitespace-nowrap [&_td:first-child]:pr-4 [&_td:first-child]:font-mono [&_td:last-child]:text-xs [&_td:last-child]:[color:color-mix(in_srgb,var(--ui-color-text)_65%,transparent)]"
291
+ >
292
+ <tr><td>$status == active</td><td>exact match</td></tr>
293
+ <tr><td>$name ^= Ada</td><td>starts with “Ada”</td></tr>
294
+ <tr><td>$email ~= @gmail.com</td><td>contains substring</td></tr>
295
+ <tr><td>$email ~= /@gmail\.com$/i</td><td>regex, case-insensitive</td></tr>
296
+ <tr><td>$age &gt;= 30</td><td>numeric compare</td></tr>
297
+ <tr><td>$age |= [21..40)</td><td>range — 21 ≤ age &lt; 40</td></tr>
298
+ <tr><td>$role |= [admin, ops]</td><td>any of a set</td></tr>
299
+ <tr><td>$tags &amp;= [urgent, vip]</td><td>all of (multi-value column)</td></tr>
300
+ <tr><td>$created &gt;= now()-7d</td><td>dynamic date (last 7 days)</td></tr>
301
+ <tr><td>$ip|inet &gt;= 10.0.0.0|inet</td><td>typed IP compare</td></tr>
302
+ <tr><td>!($role == admin)</td><td>negate a group</td></tr>
303
+ <tr><td>$status == open &amp;&amp; $priority &gt;= 3</td><td>combine with &amp;&amp; / ||</td></tr>
304
+ <tr><td>"lemon balm"</td><td>free-text search (no $)</td></tr>
305
+ </tbody>
306
+ </table>
278
307
  </div>
279
308
  <p class="text-xs [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]">
280
309
  Tip: type <kbd class="rounded border px-1 {bc}">$</kbd> for columns; enum columns suggest
@@ -1,5 +1,26 @@
1
1
  export type CompareOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "ncontains" | "starts" | "ends";
2
2
  export type Scalar = string | number | boolean;
3
+ /** Relative-time unit for a `now()` offset: seconds/minutes/hours/days/weeks. */
4
+ export type TimeUnit = "s" | "m" | "h" | "d" | "w";
5
+ /** A dynamic `now()` value — the current instant, optionally shifted by a signed offset
6
+ * (`now()-7d`, `now()+1h`). Resolved at EVALUATION time, not parse time (so it's always "now" when
7
+ * the query runs, and the server can push it down to the DB's own `now()`). Carried in the AST as a
8
+ * marker; `resolveNow` turns it into a `Date`, or the consumer's `toConditions` emits DB SQL. */
9
+ export interface NowValue {
10
+ now: true;
11
+ /** Signed amount + unit, e.g. `{ n: -7, unit: "d" }` for `now()-7d`. Absent = the current instant. */
12
+ offset?: {
13
+ n: number;
14
+ unit: TimeUnit;
15
+ };
16
+ }
17
+ /** A comparison operand: a literal scalar or a dynamic value (`now()`). */
18
+ export type Value = Scalar | NowValue;
19
+ /** Narrow a `Value` to the dynamic `now()` marker. */
20
+ export declare function isNow(v: Value): v is NowValue;
21
+ /** Resolve a `NowValue` to a concrete `Date` (defaults to the real current instant). For client-side
22
+ * evaluation; server code typically maps the marker to the DB's `now()` in `toConditions` instead. */
23
+ export declare function resolveNow(v: NowValue, at?: Date): Date;
3
24
  /** Explicit value/column cast on a comparison, e.g. `$ip |inet >= 10.0.0.0 |inet`. Storage-agnostic:
4
25
  * the query layer only carries the marker; the consumer (`toConditions`) decides the SQL (e.g.
5
26
  * Postgres `::inet`). `number`/`string`/`bool` also retype the literal at parse time. */
@@ -8,7 +29,7 @@ export type Cast = "number" | "string" | "bool" | "date" | "inet";
8
29
  export type MemberMode = "any" | "all";
9
30
  /** One end of an interval. `inclusive` reflects the bracket: `[`/`]` inclusive, `(`/`)` exclusive. */
10
31
  export type Bound = {
11
- value: Scalar;
32
+ value: Value;
12
33
  inclusive: boolean;
13
34
  };
14
35
  /** `null` bound = open/unbounded on that side (e.g. `[20..)` -> upper is null). */
@@ -29,7 +50,7 @@ export type QueryNode = {
29
50
  kind: "cmp";
30
51
  col: string;
31
52
  op: CompareOp;
32
- value: Scalar;
53
+ value: Value;
33
54
  cast?: Cast;
34
55
  } | {
35
56
  kind: "match";
@@ -41,7 +62,7 @@ export type QueryNode = {
41
62
  kind: "in";
42
63
  col: string;
43
64
  mode: MemberMode;
44
- values: Scalar[];
65
+ values: Value[];
45
66
  cast?: Cast;
46
67
  } | {
47
68
  kind: "range";
@@ -1,5 +1,22 @@
1
1
  // AST for the data-table query language. Framework-agnostic — no Svelte/Drizzle/SvelteKit deps,
2
2
  // so it runs in the browser (Query input), on the server (toConditions), and in unit tests.
3
+ /** Narrow a `Value` to the dynamic `now()` marker. */
4
+ export function isNow(v) {
5
+ return typeof v === "object" && v !== null && v.now === true;
6
+ }
7
+ const UNIT_MS = {
8
+ s: 1_000,
9
+ m: 60_000,
10
+ h: 3_600_000,
11
+ d: 86_400_000,
12
+ w: 604_800_000,
13
+ };
14
+ /** Resolve a `NowValue` to a concrete `Date` (defaults to the real current instant). For client-side
15
+ * evaluation; server code typically maps the marker to the DB's `now()` in `toConditions` instead. */
16
+ export function resolveNow(v, at = new Date()) {
17
+ const base = at.getTime();
18
+ return new Date(v.offset ? base + v.offset.n * UNIT_MS[v.offset.unit] : base);
19
+ }
3
20
  export class QueryError extends Error {
4
21
  position;
5
22
  constructor(message,
@@ -159,7 +159,35 @@ class Parser {
159
159
  }
160
160
  throw new QueryError("Expected an operator after the column", opTok.start);
161
161
  }
162
+ /** `now()` (+ optional signed offset like `-7d`, `+1h`) → a dynamic value. `now` alone (no parens)
163
+ * is an ordinary literal. Returns undefined if the cursor isn't on a `now()`. */
164
+ tryNow() {
165
+ const t = this.peek();
166
+ if (t.type !== "word" ||
167
+ t.value !== "now" ||
168
+ this.tokens[this.pos + 1]?.type !== "lparen" ||
169
+ this.tokens[this.pos + 2]?.type !== "rparen")
170
+ return undefined;
171
+ this.next(); // now
172
+ this.next(); // (
173
+ this.next(); // )
174
+ let offset;
175
+ const nx = this.peek();
176
+ if (nx.type === "word") {
177
+ const m = /^([+-])(\d+)([smhdw])$/.exec(nx.value);
178
+ if (m) {
179
+ this.next();
180
+ offset = { n: (m[1] === "-" ? -1 : 1) * parseInt(m[2], 10), unit: m[3] };
181
+ }
182
+ // a following word that ISN'T an offset is left for the outer parser (adjacent-terms error).
183
+ }
184
+ this.castAfter(); // a redundant `|date` is allowed and recorded, but not required
185
+ return { now: true, offset };
186
+ }
162
187
  parseScalar() {
188
+ const now = this.tryNow();
189
+ if (now)
190
+ return now;
163
191
  const t = this.next();
164
192
  let raw;
165
193
  let value;
@@ -232,7 +232,8 @@ Config-driven, responsive 12-column data table over a `DataGrid`.
232
232
  ### Query
233
233
 
234
234
  Filter bar over a `DataSet` — monospace input with `$column` + enum-value autocomplete and a help
235
- dialog.
235
+ dialog (operators, casts, dynamic `now()` dates, and a worked examples table). The DSL supports
236
+ `now()` / `now()±<n><unit>` dynamic dates — see the help dialog + the data spec.
236
237
 
237
238
  - **Location**: `src/lib/components/Query.svelte`
238
239
  - **Props**: `dataset` (`DataSet`), `placeholder`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.7.1-dev.35.6496cbd",
3
+ "version": "0.7.1-dev.38.b73de30",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"