@freegridjs/core 1.1.0 → 1.6.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/dist/index.js CHANGED
@@ -331,6 +331,8 @@ var FormulaError = class extends Error {
331
331
  this.code = code;
332
332
  }
333
333
  };
334
+ var ERROR_RE = /^#[A-Z/0]+[!?]$/;
335
+ var isFormulaError = (v) => typeof v === "string" && ERROR_RE.test(v);
334
336
  function tokenize(src) {
335
337
  const out = [];
336
338
  let i = 0;
@@ -366,7 +368,7 @@ function tokenize(src) {
366
368
  out.push({ t: "id", v: src.slice(i + 1, end) });
367
369
  i = end + 1;
368
370
  } else if (/[A-Za-z_À-￿]/.test(c)) {
369
- const m = /^[A-Za-z_À-￿][\wÀ-￿]*/.exec(src.slice(i));
371
+ const m = /^[A-Za-z_À-￿][\wÀ-￿.]*/.exec(src.slice(i));
370
372
  out.push({ t: "id", v: m[0] });
371
373
  i += m[0].length;
372
374
  } else {
@@ -374,8 +376,8 @@ function tokenize(src) {
374
376
  if (two === "<>" || two === "<=" || two === ">=") {
375
377
  out.push({ t: "op", v: two });
376
378
  i += 2;
377
- } else if ("+-*/%^&=<>(),".includes(c)) {
378
- out.push({ t: "op", v: c });
379
+ } else if ("+-*/%^&=<>(),;".includes(c)) {
380
+ out.push({ t: "op", v: c === ";" ? "," : c });
379
381
  i++;
380
382
  } else throw new FormulaError("#ERROR!");
381
383
  }
@@ -387,35 +389,271 @@ function num(v) {
387
389
  if (typeof v === "number") return v;
388
390
  if (typeof v === "boolean") return v ? 1 : 0;
389
391
  if (isBlankValue(v)) return 0;
392
+ if (v instanceof Date) return v.getTime();
390
393
  if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
391
394
  throw new FormulaError("#VALUE!");
392
395
  }
393
- var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : String(v);
394
- function round(x, digits) {
396
+ var text = (v) => isBlankValue(v) ? "" : typeof v === "boolean" ? v ? "TRUE" : "FALSE" : v instanceof Date ? ymd(v) : String(v);
397
+ var bool = (v) => typeof v === "boolean" ? v : typeof v === "string" && /^(true|false)$/i.test(v) ? v.toLowerCase() === "true" : num(v) !== 0;
398
+ function round(x, digits, mode = "near") {
395
399
  const d = Math.trunc(digits);
396
- const r = Number(`${Math.round(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
400
+ const f = mode === "near" ? Math.round : mode === "up" ? Math.ceil : Math.floor;
401
+ const r = Number(`${f(Number(`${Math.abs(x)}e${d}`))}e${-d}`);
397
402
  return x < 0 ? -r : r;
398
403
  }
404
+ function compare(a, b) {
405
+ const numeric = (v) => typeof v === "number" || typeof v === "boolean" || isBlankValue(v);
406
+ if (numeric(a) && numeric(b)) return num(a) - num(b);
407
+ if (a instanceof Date || b instanceof Date) return num(toDate(a)) - num(toDate(b));
408
+ return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
409
+ }
410
+ var flat = (a) => a.flatMap((v) => Array.isArray(v) ? v : [v]);
411
+ var numsLoose = (a) => a.flatMap((v) => Array.isArray(v) ? noErrors(v).filter((x) => typeof x === "number" && Number.isFinite(x)) : isBlankValue(v) ? [] : [num(v)]);
412
+ function noErrors(list) {
413
+ const bad = list.find(isFormulaError);
414
+ if (bad !== void 0) throw new FormulaError(bad);
415
+ return list;
416
+ }
417
+ function toDate(v) {
418
+ if (v instanceof Date) return v;
419
+ if (typeof v === "number") return new Date(v);
420
+ if (typeof v === "string") {
421
+ const m = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?/.exec(v.trim());
422
+ if (m) return new Date(+m[1], +m[2] - 1, +m[3], +(m[4] ?? 0), +(m[5] ?? 0), +(m[6] ?? 0));
423
+ }
424
+ throw new FormulaError("#VALUE!");
425
+ }
426
+ var pad = (n) => String(n).padStart(2, "0");
427
+ function ymd(d) {
428
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
429
+ }
430
+ var DAY = 864e5;
431
+ var dayNumber = (d) => Math.round(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate()) / DAY);
432
+ function criteria(c) {
433
+ if (typeof c !== "string") return (v) => compare(v, c) === 0 && !isBlankValue(v) === !isBlankValue(c);
434
+ const m = /^(<=|>=|<>|<|>|=)?(.*)$/s.exec(c);
435
+ const op = m[1] ?? "=";
436
+ const raw = m[2];
437
+ const target = raw.trim() !== "" && Number.isFinite(Number(raw)) ? Number(raw) : raw;
438
+ if ((op === "=" || op === "<>") && typeof target === "string" && /[*?]/.test(target)) {
439
+ const re = new RegExp(`^${target.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`, "i");
440
+ return op === "=" ? (v) => re.test(text(v)) : (v) => !re.test(text(v));
441
+ }
442
+ return (v) => {
443
+ if (op === "=" && target === "") return isBlankValue(v);
444
+ if (op === "<>" && target === "") return !isBlankValue(v);
445
+ if (typeof target === "number" && (typeof v !== "number" && !(typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))))) return op === "<>";
446
+ let r;
447
+ try {
448
+ r = compare(v, target);
449
+ } catch {
450
+ return false;
451
+ }
452
+ return op === "=" ? r === 0 : op === "<>" ? r !== 0 : op === "<" ? r < 0 : op === ">" ? r > 0 : op === "<=" ? r <= 0 : r >= 0;
453
+ };
454
+ }
455
+ function arr(v) {
456
+ if (Array.isArray(v)) return v;
457
+ throw new FormulaError("#VALUE!");
458
+ }
459
+ function ifs(a, test, crit) {
460
+ const t = arr(test), keep = criteria(crit);
461
+ const out = [];
462
+ const vals = noErrors(a.length ? arr(a[0]) : t);
463
+ t.forEach((v, i) => {
464
+ const x = vals[i];
465
+ if (keep(v) && typeof x === "number" && Number.isFinite(x)) out.push(x);
466
+ });
467
+ return out;
468
+ }
399
469
  var FUNCS = {
470
+ // math
471
+ SUM: (a) => numsLoose(a).reduce((s, v) => s + v, 0),
472
+ AVG: (a) => {
473
+ const n = numsLoose(a);
474
+ if (!n.length) throw new FormulaError("#DIV/0!");
475
+ return n.reduce((s, v) => s + v, 0) / n.length;
476
+ },
477
+ MIN: (a) => {
478
+ const n = numsLoose(a);
479
+ return n.length ? Math.min(...n) : 0;
480
+ },
481
+ MAX: (a) => {
482
+ const n = numsLoose(a);
483
+ return n.length ? Math.max(...n) : 0;
484
+ },
485
+ MEDIAN: (a) => {
486
+ const n = numsLoose(a).sort((x, y) => x - y);
487
+ if (!n.length) throw new FormulaError("#NUM!");
488
+ const m = n.length >> 1;
489
+ return n.length % 2 ? n[m] : (n[m - 1] + n[m]) / 2;
490
+ },
491
+ COUNT: (a) => flat(a).filter((v) => typeof v === "number" && Number.isFinite(v)).length,
492
+ COUNTA: (a) => flat(a).filter((v) => !isBlankValue(v)).length,
493
+ COUNTBLANK: (a) => flat(a).filter(isBlankValue).length,
494
+ PRODUCT: (a) => numsLoose(a).reduce((s, v) => s * v, 1),
400
495
  ROUND: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0),
496
+ ROUNDUP: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, "up"),
497
+ ROUNDDOWN: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, "down"),
498
+ INT: (a) => Math.floor(num(a[0])),
499
+ TRUNC: (a) => round(num(a[0]), a.length > 1 ? num(a[1]) : 0, num(a[0]) < 0 ? "up" : "down"),
401
500
  ABS: (a) => Math.abs(num(a[0])),
402
- MIN: (a) => a.length ? Math.min(...a.map(num)) : 0,
403
- MAX: (a) => a.length ? Math.max(...a.map(num)) : 0,
404
- SUM: (a) => a.reduce((s, v) => s + num(v), 0),
405
- AVG: (a) => {
406
- if (!a.length) throw new FormulaError("#DIV/0!");
407
- return a.reduce((s, v) => s + num(v), 0) / a.length;
501
+ SIGN: (a) => Math.sign(num(a[0])),
502
+ SQRT: (a) => {
503
+ const x = num(a[0]);
504
+ if (x < 0) throw new FormulaError("#NUM!");
505
+ return Math.sqrt(x);
506
+ },
507
+ POWER: (a) => num(a[0]) ** num(a[1]),
508
+ EXP: (a) => Math.exp(num(a[0])),
509
+ LN: (a) => {
510
+ const x = num(a[0]);
511
+ if (x <= 0) throw new FormulaError("#NUM!");
512
+ return Math.log(x);
513
+ },
514
+ LOG10: (a) => {
515
+ const x = num(a[0]);
516
+ if (x <= 0) throw new FormulaError("#NUM!");
517
+ return Math.log10(x);
518
+ },
519
+ MOD: (a) => {
520
+ const d = num(a[1]);
521
+ if (d === 0) throw new FormulaError("#DIV/0!");
522
+ const x = num(a[0]);
523
+ return x - d * Math.floor(x / d);
524
+ },
525
+ CEILING: (a) => {
526
+ const s = a.length > 1 ? num(a[1]) : 1;
527
+ return s === 0 ? 0 : Math.ceil(num(a[0]) / s) * s;
528
+ },
529
+ FLOOR: (a) => {
530
+ const s = a.length > 1 ? num(a[1]) : 1;
531
+ return s === 0 ? 0 : Math.floor(num(a[0]) / s) * s;
532
+ },
533
+ PI: () => Math.PI,
534
+ // text
535
+ CONCAT: (a) => flat(a).map(text).join(""),
536
+ TEXTJOIN: (a) => flat(a.slice(2)).filter((v) => !bool(a[1]) || !isBlankValue(v)).map(text).join(text(a[0])),
537
+ LEN: (a) => text(a[0]).length,
538
+ LEFT: (a) => text(a[0]).slice(0, a.length > 1 ? Math.max(0, num(a[1])) : 1),
539
+ RIGHT: (a) => {
540
+ const s = text(a[0]);
541
+ const n = a.length > 1 ? Math.max(0, num(a[1])) : 1;
542
+ return n ? s.slice(-n) : "";
543
+ },
544
+ MID: (a) => text(a[0]).substr(Math.max(0, num(a[1]) - 1), Math.max(0, num(a[2]))),
545
+ UPPER: (a) => text(a[0]).toUpperCase(),
546
+ LOWER: (a) => text(a[0]).toLowerCase(),
547
+ PROPER: (a) => text(a[0]).toLowerCase().replace(/(^|[^\p{L}])(\p{L})/gu, (_, p, c) => p + c.toUpperCase()),
548
+ TRIM: (a) => text(a[0]).trim().replace(/\s+/g, " "),
549
+ SUBSTITUTE: (a) => text(a[0]).split(text(a[1])).join(text(a[2])),
550
+ REPLACE: (a) => {
551
+ const s = text(a[0]);
552
+ const at = Math.max(0, num(a[1]) - 1);
553
+ return s.slice(0, at) + text(a[3]) + s.slice(at + Math.max(0, num(a[2])));
554
+ },
555
+ REPT: (a) => text(a[0]).repeat(Math.max(0, Math.min(1e4, num(a[1])))),
556
+ FIND: (a) => {
557
+ const i = text(a[1]).indexOf(text(a[0]), a.length > 2 ? num(a[2]) - 1 : 0);
558
+ if (i < 0) throw new FormulaError("#VALUE!");
559
+ return i + 1;
560
+ },
561
+ SEARCH: (a) => {
562
+ const i = text(a[1]).toLowerCase().indexOf(text(a[0]).toLowerCase(), a.length > 2 ? num(a[2]) - 1 : 0);
563
+ if (i < 0) throw new FormulaError("#VALUE!");
564
+ return i + 1;
565
+ },
566
+ VALUE: (a) => num(text(a[0]).replace(/,/g, "")),
567
+ TEXT: (a) => {
568
+ const v = a[0], f = text(a[1]);
569
+ const dec = /0\.(0+)/.exec(f)?.[1].length ?? 0;
570
+ if (/[yd]/i.test(f)) {
571
+ const d = toDate(v);
572
+ return f.replace(/yyyy/gi, String(d.getFullYear())).replace(/mm/g, pad(d.getMonth() + 1)).replace(/dd/gi, pad(d.getDate()));
573
+ }
574
+ const n = num(v);
575
+ const s = f.includes("%") ? (n * 100).toFixed(dec) + "%" : f.includes(",") ? n.toLocaleString("en-US", { minimumFractionDigits: dec, maximumFractionDigits: dec }) : n.toFixed(dec);
576
+ return s;
577
+ },
578
+ // logic and types
579
+ AND: (a) => flat(a).every(bool),
580
+ OR: (a) => flat(a).some(bool),
581
+ XOR: (a) => flat(a).filter(bool).length % 2 === 1,
582
+ NOT: (a) => !bool(a[0]),
583
+ ISBLANK: (a) => isBlankValue(a[0]),
584
+ ISNUMBER: (a) => typeof a[0] === "number" && Number.isFinite(a[0]),
585
+ ISTEXT: (a) => typeof a[0] === "string" && !isFormulaError(a[0]),
586
+ ISERROR: (a) => isFormulaError(a[0]),
587
+ // dates (as "YYYY-MM-DD" text, like date columns)
588
+ TODAY: () => ymd(/* @__PURE__ */ new Date()),
589
+ NOW: () => /* @__PURE__ */ new Date(),
590
+ DATE: (a) => ymd(new Date(num(a[0]), num(a[1]) - 1, num(a[2]))),
591
+ YEAR: (a) => toDate(a[0]).getFullYear(),
592
+ MONTH: (a) => toDate(a[0]).getMonth() + 1,
593
+ DAY: (a) => toDate(a[0]).getDate(),
594
+ WEEKDAY: (a) => toDate(a[0]).getDay() + 1,
595
+ DAYS: (a) => dayNumber(toDate(a[0])) - dayNumber(toDate(a[1])),
596
+ EDATE: (a) => {
597
+ const d = toDate(a[0]);
598
+ const day = d.getDate();
599
+ const r = new Date(d.getFullYear(), d.getMonth() + num(a[1]), 1);
600
+ r.setDate(Math.min(day, new Date(r.getFullYear(), r.getMonth() + 1, 0).getDate()));
601
+ return ymd(r);
602
+ },
603
+ EOMONTH: (a) => {
604
+ const d = toDate(a[0]);
605
+ return ymd(new Date(d.getFullYear(), d.getMonth() + num(a[1]) + 1, 0));
606
+ },
607
+ // over columns
608
+ SUMIF: (a) => ifs(a.slice(2), arr(a[0]), a[1]).reduce((s, v) => s + v, 0),
609
+ COUNTIF: (a) => {
610
+ const keep = criteria(a[1]);
611
+ return arr(a[0]).filter(keep).length;
612
+ },
613
+ AVERAGEIF: (a) => {
614
+ const n = ifs(a.slice(2), arr(a[0]), a[1]);
615
+ if (!n.length) throw new FormulaError("#DIV/0!");
616
+ return n.reduce((s, v) => s + v, 0) / n.length;
617
+ },
618
+ LOOKUP: (a) => {
619
+ const keys = arr(a[1]), vals = a.length > 2 ? arr(a[2]) : keys;
620
+ const i = keys.findIndex((k) => {
621
+ try {
622
+ return compare(k, a[0]) === 0;
623
+ } catch {
624
+ return false;
625
+ }
626
+ });
627
+ if (i < 0) {
628
+ if (a.length > 3) return a[3];
629
+ throw new FormulaError("#N/A");
630
+ }
631
+ return vals[i];
632
+ },
633
+ RANK: (a) => {
634
+ const v = num(a[0]);
635
+ const list = numsLoose([arr(a[1])]);
636
+ const asc = a.length > 2 && bool(a[2]);
637
+ return 1 + list.filter((x) => asc ? x < v : x > v).length;
408
638
  }
409
639
  };
410
- function compare(a, b) {
411
- const bothNum = (typeof a === "number" || isBlankValue(a)) && (typeof b === "number" || isBlankValue(b));
412
- if (bothNum) return num(a) - num(b);
413
- return text(a).localeCompare(text(b), void 0, { sensitivity: "accent" });
414
- }
640
+ FUNCS.AVERAGE = FUNCS.AVG;
641
+ FUNCS.XLOOKUP = FUNCS.LOOKUP;
642
+ FUNCS.CONCATENATE = FUNCS.CONCAT;
643
+ var custom = /* @__PURE__ */ new Map();
644
+ function registerFormulaFunction(name, fn) {
645
+ if (typeof name !== "string" || !/^[A-Za-z_][\w.]*$/.test(name) || typeof fn !== "function") return;
646
+ custom.set(name.toUpperCase(), fn);
647
+ compiled.clear();
648
+ }
649
+ function formulaFunctions() {
650
+ return [.../* @__PURE__ */ new Set([...Object.keys(FUNCS), ...LAZY, ...SPECIAL, ...custom.keys()])].sort();
651
+ }
652
+ var LAZY = ["IF", "IFS", "IFERROR", "IFNA", "SWITCH", "CHOOSE"];
653
+ var SPECIAL = ["COL", "PREV", "NEXT", "ROW"];
415
654
  var Parser = class {
416
- constructor(tokens, resolve) {
655
+ constructor(tokens) {
417
656
  this.tokens = tokens;
418
- this.resolve = resolve;
419
657
  this.i = 0;
420
658
  }
421
659
  parse() {
@@ -435,52 +673,46 @@ var Parser = class {
435
673
  comparison() {
436
674
  const a = this.concat();
437
675
  for (const op of ["=", "<>", "<=", ">=", "<", ">"]) {
438
- if (this.take(op)) {
439
- const c = compare(a, this.concat());
440
- return op === "=" ? c === 0 : op === "<>" ? c !== 0 : op === "<" ? c < 0 : op === ">" ? c > 0 : op === "<=" ? c <= 0 : c >= 0;
441
- }
676
+ if (this.take(op)) return { k: "bin", op, a, b: this.concat() };
442
677
  }
443
678
  return a;
444
679
  }
445
680
  concat() {
446
681
  let v = this.additive();
447
- while (this.take("&")) v = text(v) + text(this.additive());
682
+ while (this.take("&")) v = { k: "bin", op: "&", a: v, b: this.additive() };
448
683
  return v;
449
684
  }
450
685
  additive() {
451
686
  let v = this.multiplicative();
452
687
  for (; ; ) {
453
- if (this.take("+")) v = num(v) + num(this.multiplicative());
454
- else if (this.take("-")) v = num(v) - num(this.multiplicative());
688
+ if (this.take("+")) v = { k: "bin", op: "+", a: v, b: this.multiplicative() };
689
+ else if (this.take("-")) v = { k: "bin", op: "-", a: v, b: this.multiplicative() };
455
690
  else return v;
456
691
  }
457
692
  }
458
693
  multiplicative() {
459
694
  let v = this.unary();
460
695
  for (; ; ) {
461
- if (this.take("*")) v = num(v) * num(this.unary());
462
- else if (this.take("/") || this.take("%")) {
463
- const op = this.tokens[this.i - 1].v;
464
- const d = num(this.unary());
465
- if (d === 0) throw new FormulaError("#DIV/0!");
466
- v = op === "/" ? num(v) / d : num(v) % d;
467
- } else return v;
696
+ if (this.take("*")) v = { k: "bin", op: "*", a: v, b: this.unary() };
697
+ else if (this.take("/")) v = { k: "bin", op: "/", a: v, b: this.unary() };
698
+ else if (this.take("%")) v = { k: "bin", op: "%", a: v, b: this.unary() };
699
+ else return v;
468
700
  }
469
701
  }
470
702
  unary() {
471
- if (this.take("-")) return -num(this.unary());
472
- if (this.take("+")) return num(this.unary());
703
+ if (this.take("-")) return { k: "un", op: "-", a: this.unary() };
704
+ if (this.take("+")) return { k: "un", op: "+", a: this.unary() };
473
705
  return this.power();
474
706
  }
475
707
  power() {
476
708
  const base = this.primary();
477
- if (this.take("^")) return num(base) ** num(this.unary());
709
+ if (this.take("^")) return { k: "bin", op: "^", a: base, b: this.unary() };
478
710
  return base;
479
711
  }
480
712
  primary() {
481
713
  const t = this.tokens[this.i++];
482
714
  if (!t) throw new FormulaError("#ERROR!");
483
- if (t.t === "num" || t.t === "str") return t.v;
715
+ if (t.t === "num" || t.t === "str") return { k: "lit", v: t.v };
484
716
  if (t.t === "op") {
485
717
  if (t.v !== "(") throw new FormulaError("#ERROR!");
486
718
  const v = this.comparison();
@@ -498,46 +730,296 @@ var Parser = class {
498
730
  if (!this.take(")")) throw new FormulaError("#ERROR!");
499
731
  }
500
732
  const upper = name.toUpperCase();
501
- if (upper === "IF") {
502
- if (args.length < 2) throw new FormulaError("#ERROR!");
503
- const cond = typeof args[0] === "boolean" ? args[0] : num(args[0]) !== 0;
504
- return cond ? args[1] : args.length > 2 ? args[2] : false;
733
+ if (upper === "COL" || upper === "PREV" || upper === "NEXT") {
734
+ if (!args.length || args[0].k !== "ref") throw new FormulaError("#ERROR!");
505
735
  }
506
- const fn = Object.hasOwn(FUNCS, upper) ? FUNCS[upper] : void 0;
507
- if (!fn) throw new FormulaError("#NAME?");
508
- return fn(args);
736
+ const rowFree = upper !== "PREV" && upper !== "NEXT" && upper !== "ROW" && args.every((a) => isRowFree(a, upper === "COL"));
737
+ return { k: "call", name: upper, args, rowFree };
509
738
  }
510
- if (/^(true|false)$/i.test(name)) return name.toLowerCase() === "true";
511
- return this.resolve(name);
739
+ if (/^(true|false)$/i.test(name)) return { k: "lit", v: name.toLowerCase() === "true" };
740
+ return { k: "ref", name };
512
741
  }
513
742
  };
743
+ function isRowFree(n, inCol = false) {
744
+ if (n.k === "lit") return true;
745
+ if (n.k === "ref") return inCol;
746
+ if (n.k === "un") return isRowFree(n.a);
747
+ if (n.k === "bin") return isRowFree(n.a) && isRowFree(n.b);
748
+ return n.rowFree;
749
+ }
750
+ var compiled = /* @__PURE__ */ new Map();
751
+ var MAX_COMPILED = 2e3;
752
+ function compile(formula) {
753
+ let n = compiled.get(formula);
754
+ if (!n) {
755
+ try {
756
+ n = new Parser(tokenize(formula.slice(1))).parse();
757
+ } catch (e) {
758
+ n = e instanceof FormulaError ? e : new FormulaError("#ERROR!");
759
+ }
760
+ if (compiled.size >= MAX_COMPILED) compiled.delete(compiled.keys().next().value);
761
+ compiled.set(formula, n);
762
+ }
763
+ if (n instanceof FormulaError) throw n;
764
+ return n;
765
+ }
766
+ var caches = /* @__PURE__ */ new WeakMap();
767
+ function cacheOf(scope) {
768
+ const data = scope.rows();
769
+ const version2 = scope.version();
770
+ let c = caches.get(scope);
771
+ if (!c || c.data !== data || c.version !== version2) {
772
+ c = { data, version: version2, index: null, cols: /* @__PURE__ */ new Map(), values: /* @__PURE__ */ new WeakMap(), rowFree: /* @__PURE__ */ new Map(), filled: /* @__PURE__ */ new Map() };
773
+ caches.set(scope, c);
774
+ }
775
+ return c;
776
+ }
777
+ var scopes = /* @__PURE__ */ new WeakMap();
778
+ function setFormulaScope(defs, scope) {
779
+ for (const d of defs) if (d && typeof d === "object") scopes.set(d, scope);
780
+ }
514
781
  var MAX_DEPTH = 32;
515
- function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0, memo = /* @__PURE__ */ new Map()) {
782
+ function cellOf(ctx, name) {
783
+ const { row, scope } = ctx;
784
+ if (row && Object.hasOwn(row, name)) {
785
+ const v = row[name];
786
+ if (typeof v === "string" && v.startsWith("=") && v.length > 1) return formulaOf(ctx, name, v);
787
+ return v;
788
+ }
789
+ const f = scope?.computed(name);
790
+ if (f !== void 0) return formulaOf(ctx, name, f);
791
+ throw new FormulaError("#REF!");
792
+ }
793
+ function formulaOf(ctx, name, formula) {
794
+ if (ctx.seen.has(name) || ctx.depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
795
+ let r;
796
+ if (ctx.memo.has(name)) r = ctx.memo.get(name);
797
+ else {
798
+ const shared = ctx.scope ? cacheOf(ctx.scope).values.get(ctx.row) : void 0;
799
+ if (shared?.has(name)) r = shared.get(name);
800
+ else {
801
+ r = run(formula, { ...ctx, seen: /* @__PURE__ */ new Set([...ctx.seen, name]), depth: ctx.depth + 1 });
802
+ ctx.memo.set(name, r);
803
+ }
804
+ }
805
+ if (isFormulaError(r)) throw new FormulaError(r);
806
+ return r;
807
+ }
808
+ function cellAt(ctx, index, name) {
809
+ const scope = ctx.scope;
810
+ if (!scope) throw new FormulaError("#REF!");
811
+ const c = cacheOf(scope);
812
+ if (index < 0 || index >= c.data.length) return null;
813
+ const row = c.data[index];
814
+ const done = c.filled.get(name) ?? -1;
815
+ if (index > done + 1 && index - done > 1) {
816
+ for (let k = done + 1; k < index; k++) valueIn(scope, c, c.data[k], name);
817
+ }
818
+ const v = valueIn(scope, c, row, name);
819
+ if (index > (c.filled.get(name) ?? -1)) c.filled.set(name, index);
820
+ if (isFormulaError(v)) throw new FormulaError(v);
821
+ return v;
822
+ }
823
+ function valueIn(scope, c, row, name) {
824
+ let m = c.values.get(row);
825
+ if (m?.has(name)) return m.get(name);
826
+ let v;
516
827
  try {
517
- const resolve = (name) => {
518
- if (!row || !Object.hasOwn(row, name)) throw new FormulaError("#REF!");
519
- const v2 = row[name];
520
- if (typeof v2 === "string" && v2.startsWith("=")) {
521
- if (seen2.has(name) || depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
522
- let r;
523
- if (memo.has(name)) r = memo.get(name);
524
- else {
525
- r = evaluateFormula(v2, row, /* @__PURE__ */ new Set([...seen2, name]), depth + 1, memo);
526
- memo.set(name, r);
828
+ v = cellOf({ row, scope, seen: /* @__PURE__ */ new Set(), depth: 0, memo: /* @__PURE__ */ new Map() }, name);
829
+ } catch (e) {
830
+ v = e instanceof FormulaError ? e.code : "#ERROR!";
831
+ }
832
+ if (!m) c.values.set(row, m = /* @__PURE__ */ new Map());
833
+ m.set(name, v);
834
+ return v;
835
+ }
836
+ function indexOf(ctx) {
837
+ if (!ctx.scope) throw new FormulaError("#REF!");
838
+ const c = cacheOf(ctx.scope);
839
+ if (!c.index) c.index = new Map(c.data.map((r, i2) => [r, i2]));
840
+ const i = c.index.get(ctx.row);
841
+ if (i === void 0) throw new FormulaError("#REF!");
842
+ return i;
843
+ }
844
+ function column(ctx, name) {
845
+ if (!ctx.scope) throw new FormulaError("#REF!");
846
+ const scope = ctx.scope;
847
+ const c = cacheOf(scope);
848
+ let col = c.cols.get(name);
849
+ if (!col) {
850
+ c.cols.set(name, CYCLE);
851
+ col = c.data.map((r) => valueIn(scope, c, r, name));
852
+ c.cols.set(name, col);
853
+ }
854
+ if (col === CYCLE) throw new FormulaError("#CYCLE!");
855
+ return col;
856
+ }
857
+ var CYCLE = [];
858
+ function evaluate(n, ctx) {
859
+ switch (n.k) {
860
+ case "lit":
861
+ return n.v;
862
+ case "ref":
863
+ return cellOf(ctx, n.name);
864
+ case "un":
865
+ return n.op === "-" ? -num(evaluate(n.a, ctx)) : num(evaluate(n.a, ctx));
866
+ case "bin": {
867
+ const a = evaluate(n.a, ctx), b = evaluate(n.b, ctx);
868
+ switch (n.op) {
869
+ case "+":
870
+ return num(a) + num(b);
871
+ case "-":
872
+ return num(a) - num(b);
873
+ case "*":
874
+ return num(a) * num(b);
875
+ case "/":
876
+ case "%": {
877
+ const d = num(b);
878
+ if (d === 0) throw new FormulaError("#DIV/0!");
879
+ return n.op === "/" ? num(a) / d : num(a) % d;
880
+ }
881
+ case "^":
882
+ return num(a) ** num(b);
883
+ case "&":
884
+ return text(a) + text(b);
885
+ default: {
886
+ const c = compare(a, b);
887
+ return n.op === "=" ? c === 0 : n.op === "<>" ? c !== 0 : n.op === "<" ? c < 0 : n.op === ">" ? c > 0 : n.op === "<=" ? c <= 0 : c >= 0;
527
888
  }
528
- if (typeof r === "string" && /^#[A-Z/0]+[!?]$/.test(r)) throw new FormulaError(r);
529
- return r;
530
889
  }
531
- return v2;
532
- };
533
- const v = new Parser(tokenize(formula.slice(1)), resolve).parse();
890
+ }
891
+ case "call": {
892
+ if (n.rowFree && ctx.scope) {
893
+ const c = cacheOf(ctx.scope);
894
+ if (c.rowFree.has(n)) {
895
+ const v = c.rowFree.get(n);
896
+ if (v instanceof FormulaError) throw v;
897
+ return v;
898
+ }
899
+ try {
900
+ const v = call(n, ctx);
901
+ c.rowFree.set(n, v);
902
+ return v;
903
+ } catch (e) {
904
+ if (e instanceof FormulaError) c.rowFree.set(n, e);
905
+ throw e;
906
+ }
907
+ }
908
+ return call(n, ctx);
909
+ }
910
+ }
911
+ }
912
+ function call(n, ctx) {
913
+ const { name, args } = n;
914
+ const ev = (i) => evaluate(args[i], ctx);
915
+ switch (name) {
916
+ case "IF": {
917
+ if (args.length < 2) throw new FormulaError("#ERROR!");
918
+ return bool(ev(0)) ? ev(1) : args.length > 2 ? ev(2) : false;
919
+ }
920
+ case "IFS":
921
+ for (let i = 0; i + 1 < args.length; i += 2) if (bool(ev(i))) return ev(i + 1);
922
+ throw new FormulaError("#N/A");
923
+ case "IFERROR":
924
+ case "IFNA": {
925
+ try {
926
+ const v = ev(0);
927
+ if (isFormulaError(v) && (name === "IFERROR" || v === "#N/A")) return args.length > 1 ? ev(1) : "";
928
+ return v;
929
+ } catch (e) {
930
+ if (!(e instanceof FormulaError) || name === "IFNA" && e.code !== "#N/A") throw e;
931
+ return args.length > 1 ? ev(1) : "";
932
+ }
933
+ }
934
+ case "SWITCH": {
935
+ const v = ev(0);
936
+ for (let i = 1; i + 1 < args.length; i += 2) if (compare(v, ev(i)) === 0) return ev(i + 1);
937
+ if (args.length % 2 === 0) return ev(args.length - 1);
938
+ throw new FormulaError("#N/A");
939
+ }
940
+ case "CHOOSE": {
941
+ const i = Math.trunc(num(ev(0)));
942
+ if (i < 1 || i >= args.length) throw new FormulaError("#VALUE!");
943
+ return ev(i);
944
+ }
945
+ case "COL":
946
+ return column(ctx, args[0].name);
947
+ case "ROW":
948
+ return indexOf(ctx) + 1;
949
+ case "PREV":
950
+ case "NEXT": {
951
+ const step = args.length > 1 ? Math.trunc(num(ev(1))) : 1;
952
+ const at = indexOf(ctx) + (name === "PREV" ? -step : step);
953
+ const fieldName = args[0].name;
954
+ if (name === "NEXT" && ctx.depth >= MAX_DEPTH) throw new FormulaError("#CYCLE!");
955
+ return name === "PREV" ? cellAt(ctx, at, fieldName) : nextCell(ctx, at, fieldName);
956
+ }
957
+ }
958
+ const fn = (Object.hasOwn(FUNCS, name) ? FUNCS[name] : void 0) ?? custom.get(name);
959
+ if (!fn) throw new FormulaError("#NAME?");
960
+ const values = args.map((_, i) => ev(i));
961
+ if (custom.has(name) && !Object.hasOwn(FUNCS, name)) {
962
+ try {
963
+ return fn(values);
964
+ } catch (e) {
965
+ throw e instanceof FormulaError ? e : new FormulaError("#VALUE!");
966
+ }
967
+ }
968
+ return fn(values);
969
+ }
970
+ function nextCell(ctx, index, name) {
971
+ const c = ctx.scope ? cacheOf(ctx.scope) : null;
972
+ if (!c) throw new FormulaError("#REF!");
973
+ if (index < 0 || index >= c.data.length) return null;
974
+ return cellOf({ ...ctx, row: c.data[index], seen: /* @__PURE__ */ new Set(), depth: ctx.depth + 1, memo: /* @__PURE__ */ new Map() }, name);
975
+ }
976
+ function run(formula, ctx) {
977
+ try {
978
+ const v = evaluate(compile(formula), ctx);
534
979
  if (typeof v === "number" && !Number.isFinite(v)) return "#NUM!";
980
+ if (Array.isArray(v)) return "#VALUE!";
535
981
  return v;
536
982
  } catch (e) {
983
+ if (e instanceof RangeError) return "#CYCLE!";
537
984
  return e instanceof FormulaError ? e.code : "#ERROR!";
538
985
  }
539
986
  }
987
+ function evaluateFormula(formula, row, seen2 = /* @__PURE__ */ new Set(), depth = 0, memo = /* @__PURE__ */ new Map(), scope = null) {
988
+ return run(formula, { row, scope, seen: seen2, depth, memo });
989
+ }
990
+ function columnFormulaValue(def, row) {
991
+ const scope = scopes.get(def) ?? null;
992
+ const id = def.id ?? def.field;
993
+ if (def.formula) {
994
+ if (scope && id) {
995
+ const c = cacheOf(scope);
996
+ return valueIn(scope, c, row, id);
997
+ }
998
+ return run(def.formula, { row, scope, seen: new Set(id ? [id] : []), depth: 0, memo: /* @__PURE__ */ new Map() });
999
+ }
1000
+ if (scope) return valueIn(scope, cacheOf(scope), row, def.field);
1001
+ return run(row[def.field], { row, scope, seen: /* @__PURE__ */ new Set([def.field]), depth: 0, memo: /* @__PURE__ */ new Map() });
1002
+ }
540
1003
  var isFormula = (v) => typeof v === "string" && v.length > 1 && v.startsWith("=");
1004
+ function formulaRefs(formula) {
1005
+ let n;
1006
+ try {
1007
+ n = compile(formula);
1008
+ } catch {
1009
+ return null;
1010
+ }
1011
+ const out = /* @__PURE__ */ new Set();
1012
+ const walk = (x) => {
1013
+ if (x.k === "ref") out.add(x.name);
1014
+ else if (x.k === "un") walk(x.a);
1015
+ else if (x.k === "bin") {
1016
+ walk(x.a);
1017
+ walk(x.b);
1018
+ } else if (x.k === "call") x.args.forEach(walk);
1019
+ };
1020
+ walk(n);
1021
+ return [...out];
1022
+ }
541
1023
 
542
1024
  // src/rows/values.ts
543
1025
  function getCellValue(def, row) {
@@ -549,9 +1031,10 @@ function getCellValue(def, row) {
549
1031
  return void 0;
550
1032
  }
551
1033
  }
1034
+ if (typeof def.formula === "string" && isFormula(def.formula)) return columnFormulaValue(def, row);
552
1035
  if (def.field == null) return void 0;
553
1036
  const raw = row[def.field];
554
- return def.formulas && isFormula(raw) ? evaluateFormula(raw, row, /* @__PURE__ */ new Set([def.field])) : raw;
1037
+ return def.formulas && isFormula(raw) ? columnFormulaValue(def, row) : raw;
555
1038
  }
556
1039
 
557
1040
  // src/interactions/filterRow.ts
@@ -764,13 +1247,13 @@ var FilterRow = class {
764
1247
  function isBlank(v) {
765
1248
  return v == null || typeof v === "string" && v.trim() === "" || typeof v === "number" && Number.isNaN(v);
766
1249
  }
767
- var pad = (n) => String(n).padStart(2, "0");
1250
+ var pad2 = (n) => String(n).padStart(2, "0");
768
1251
  function toDateKey2(v) {
769
1252
  if (isBlank(v)) return null;
770
1253
  if (typeof v === "string" && /^\d{4}-\d{2}-\d{2}$/.test(v)) return v;
771
1254
  const d = v instanceof Date ? v : new Date(v);
772
1255
  if (Number.isNaN(d.getTime())) return null;
773
- return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1256
+ return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
774
1257
  }
775
1258
  function setKey(v) {
776
1259
  if (isBlank(v)) return "";
@@ -925,7 +1408,7 @@ var DATE_PRESETS = {
925
1408
  medium: { dateStyle: "medium" },
926
1409
  long: { dateStyle: "long" }
927
1410
  };
928
- function toDate(v) {
1411
+ function toDate2(v) {
929
1412
  if (isBlank(v)) return null;
930
1413
  if (v instanceof Date) return Number.isNaN(v.getTime()) ? null : v;
931
1414
  if (typeof v === "string") {
@@ -954,13 +1437,13 @@ function create(type, format, locale) {
954
1437
  }
955
1438
  case "date": {
956
1439
  if (locale.toLowerCase().startsWith("ko") && (format === void 0 || format === "date" || format === "datetime")) {
957
- const pad3 = (n) => String(n).padStart(2, "0");
1440
+ const pad4 = (n) => String(n).padStart(2, "0");
958
1441
  const withTime = format === "datetime";
959
1442
  return (v) => {
960
- const d = toDate(v);
1443
+ const d = toDate2(v);
961
1444
  if (!d) return isBlank(v) ? "" : String(v);
962
- const day = `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}`;
963
- return withTime ? `${day} ${pad3(d.getHours())}:${pad3(d.getMinutes())}` : day;
1445
+ const day = `${d.getFullYear()}-${pad4(d.getMonth() + 1)}-${pad4(d.getDate())}`;
1446
+ return withTime ? `${day} ${pad4(d.getHours())}:${pad4(d.getMinutes())}` : day;
964
1447
  };
965
1448
  }
966
1449
  let opts = DATE_PRESETS.date;
@@ -970,7 +1453,7 @@ function create(type, format, locale) {
970
1453
  } else if (format) opts = format;
971
1454
  const df = new Intl.DateTimeFormat(locale, opts);
972
1455
  return (v) => {
973
- const d = toDate(v);
1456
+ const d = toDate2(v);
974
1457
  return d ? df.format(d) : isBlank(v) ? "" : String(v);
975
1458
  };
976
1459
  }
@@ -1173,6 +1656,8 @@ var Renderer = class {
1173
1656
  this.frame = 0;
1174
1657
  this.headerDirty = true;
1175
1658
  this.version = 0;
1659
+ /** Per-row repaint counters on top of `version` (live updates paint only the rows that changed). */
1660
+ this.rowVersions = /* @__PURE__ */ new Map();
1176
1661
  this.lastRange = { first: 0, last: -1 };
1177
1662
  /** Pinned summary section (top or bottom) and its per-region inners. */
1178
1663
  this.summaryEl = null;
@@ -1298,7 +1783,15 @@ var Renderer = class {
1298
1783
  }
1299
1784
  invalidate(what = {}) {
1300
1785
  if (what.header) this.headerDirty = true;
1301
- if (what.rows) this.version++;
1786
+ if (what.rows) {
1787
+ this.version++;
1788
+ this.rowVersions.clear();
1789
+ }
1790
+ this.schedule();
1791
+ }
1792
+ /** Paint these rows again on the next frame; the others stay as they are. */
1793
+ invalidateRowIds(ids) {
1794
+ for (const id of ids) this.rowVersions.set(id, (this.rowVersions.get(id) ?? 0) + 1);
1302
1795
  this.schedule();
1303
1796
  }
1304
1797
  schedule() {
@@ -1416,18 +1909,18 @@ var Renderer = class {
1416
1909
  return;
1417
1910
  }
1418
1911
  const filtered = !rowModel.server && store.state.data.length > 0;
1419
- const custom = kind === "loading" ? options.loadingOverlay : options.noRowsOverlay;
1912
+ const custom2 = kind === "loading" ? options.loadingOverlay : options.noRowsOverlay;
1420
1913
  const key = `${kind}|${filtered}|${store.state.locale.code}`;
1421
1914
  if (key === this.overlayKey) return;
1422
1915
  this.overlayKey = key;
1423
- this.overlay.classList.toggle("fg-overlay-custom", typeof custom === "function");
1424
- if (custom === void 0 || custom === null) {
1916
+ this.overlay.classList.toggle("fg-overlay-custom", typeof custom2 === "function");
1917
+ if (custom2 === void 0 || custom2 === null) {
1425
1918
  this.overlay.textContent = kind === "loading" ? store.state.locale.loading : store.state.locale.noRows;
1426
- } else if (typeof custom === "string") {
1427
- this.overlay.textContent = custom;
1919
+ } else if (typeof custom2 === "string") {
1920
+ this.overlay.textContent = custom2;
1428
1921
  } else {
1429
1922
  try {
1430
- const out = custom({ filtered });
1923
+ const out = custom2({ filtered });
1431
1924
  if (typeof out === "string") this.overlay.textContent = out;
1432
1925
  else this.overlay.replaceChildren(out);
1433
1926
  } catch {
@@ -1645,7 +2138,7 @@ var Renderer = class {
1645
2138
  const inRange = hits.map((b) => `${b.c1}-${b.c2}`).join(",");
1646
2139
  const merge2 = this.mergeState(spans, cols, i, range.first);
1647
2140
  const pos = node.kind === "detail" ? `${this.scroller.scrollLeft}:${this.scroller.clientWidth}` : "";
1648
- const key = `${node.id}|${this.version}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
2141
+ const key = `${node.id}|${this.version}.${this.rowVersions.get(node.id) ?? 0}|${colKey}|${isSel ? 1 : 0}${node.kind === "group" ? `:${this.grid.selectionVersion}` : ""}|${focusCol}|${inRange}|${merge2}|${pos}`;
1649
2142
  let row = map.get(i);
1650
2143
  if (!row) {
1651
2144
  const rowEl = this.pool[r].pop() ?? el("div", "fg-row", { role: "row" });
@@ -1775,8 +2268,11 @@ var Renderer = class {
1775
2268
  if (def.wrapText) cls += " fg-cell-wrap";
1776
2269
  if (typeof def.editable === "function" && i >= 0 && !cellEditable(def, node.data, i)) cls += " fg-cell-readonly";
1777
2270
  if (i >= 0) cls += this.grid.finder.classFor(i, col.id);
1778
- if (this.grid.isFlashing(node.id, col.id)) cls += " fg-cell-flash";
2271
+ cls += this.grid.flashClass(node.id, col.id);
2272
+ const cf = i >= 0 && !node.kind ? this.grid.cellFormat(node.data, col.id, value) : null;
2273
+ if (cf?.bar) cls += " fg-cf-bar";
1779
2274
  cell.className = cls;
2275
+ this.applyFormat(cell, cf);
1780
2276
  if (def.sparkline && !def.renderer) {
1781
2277
  const nums = Array.isArray(value) ? value.map(Number).filter(Number.isFinite) : [];
1782
2278
  if (nums.length) cell.replaceChildren(sparkline(nums, def.sparkline === true ? {} : def.sparkline, col.width, this.opts.rowHeight));
@@ -1786,6 +2282,12 @@ var Renderer = class {
1786
2282
  }
1787
2283
  if (!def.renderer) {
1788
2284
  cell.textContent = formatted;
2285
+ if (cf?.icon) {
2286
+ const icon = el("span", "fg-cf-icon", { "aria-hidden": "true" });
2287
+ icon.textContent = cf.icon.char;
2288
+ icon.style.color = cf.icon.color;
2289
+ cell.prepend(icon);
2290
+ }
1789
2291
  return;
1790
2292
  }
1791
2293
  try {
@@ -1797,6 +2299,36 @@ var Renderer = class {
1797
2299
  cell.textContent = "";
1798
2300
  }
1799
2301
  }
2302
+ /** Inline styles of a conditional format; pooled cells are reset first. */
2303
+ applyFormat(cell, cf) {
2304
+ const st = cell.style;
2305
+ if (!cf) {
2306
+ if (cell.dataset.cf) {
2307
+ st.color = st.background = st.fontWeight = st.fontStyle = st.textDecoration = "";
2308
+ st.removeProperty("--fg-bar-from");
2309
+ st.removeProperty("--fg-bar-to");
2310
+ st.removeProperty("--fg-bar-color");
2311
+ delete cell.dataset.cf;
2312
+ }
2313
+ return;
2314
+ }
2315
+ cell.dataset.cf = "1";
2316
+ const s = cf.style;
2317
+ st.color = s.color ?? "";
2318
+ st.background = s.background ?? "";
2319
+ st.fontWeight = s.bold ? "600" : "";
2320
+ st.fontStyle = s.italic ? "italic" : "";
2321
+ st.textDecoration = s.strike ? "line-through" : "";
2322
+ if (cf.bar) {
2323
+ st.setProperty("--fg-bar-from", `${(cf.bar.from * 100).toFixed(2)}%`);
2324
+ st.setProperty("--fg-bar-to", `${(cf.bar.to * 100).toFixed(2)}%`);
2325
+ st.setProperty("--fg-bar-color", cf.bar.color);
2326
+ } else {
2327
+ st.removeProperty("--fg-bar-from");
2328
+ st.removeProperty("--fg-bar-to");
2329
+ st.removeProperty("--fg-bar-color");
2330
+ }
2331
+ }
1800
2332
  cellAt(target) {
1801
2333
  const cell = target instanceof Element ? target.closest(".fg-cell") : null;
1802
2334
  const row = cell?.parentElement;
@@ -2236,9 +2768,9 @@ var Renderer = class {
2236
2768
  }
2237
2769
  const { scroller, canvas } = this.cards;
2238
2770
  const cols = grid.columns.displayed().filter((c) => !isSystemCol(c.id));
2239
- const line = Math.round(this.opts.rowHeight * 0.75);
2771
+ const line2 = Math.round(this.opts.rowHeight * 0.75);
2240
2772
  const select = !!grid.options.rowSelection;
2241
- const cardH = 20 + cols.length * line + (select ? line : 0);
2773
+ const cardH = 20 + cols.length * line2 + (select ? line2 : 0);
2242
2774
  const gap = 10;
2243
2775
  canvas.style.height = `${count * (cardH + gap)}px`;
2244
2776
  const first = Math.max(0, Math.floor(scroller.scrollTop / (cardH + gap)) - 2);
@@ -2252,7 +2784,7 @@ var Renderer = class {
2252
2784
  card.dataset.row = String(i);
2253
2785
  card.style.transform = `translateY(${i * (cardH + gap)}px)`;
2254
2786
  card.style.height = `${cardH}px`;
2255
- card.style.setProperty("--fg-card-line", `${line}px`);
2787
+ card.style.setProperty("--fg-card-line", `${line2}px`);
2256
2788
  if (node.kind === "group" && node.group) {
2257
2789
  const def = grid.columns.getNode(node.group.colId)?.def;
2258
2790
  card.textContent = `${def?.header ?? node.group.colId}: ${String(node.group.value ?? locale.blanks)} (${node.group.rows.length})`;
@@ -2599,6 +3131,22 @@ var en = {
2599
3131
  chartCombo: "Bar and line chart",
2600
3132
  chartScatter: "Scatter chart",
2601
3133
  freeBadge: "Free version",
3134
+ exportPdf: "Export PDF",
3135
+ exportJson: "Export JSON",
3136
+ ai: { title: "AI assistant", placeholder: "Ask about the data or tell the grid what to do", send: "Send", thinking: "Working\u2026", undo: "Undo", undone: "Undone", failed: "The assistant did not answer", skipped: "Skipped", done: "Done" },
3137
+ cf: {
3138
+ title: "Conditional formatting",
3139
+ add: "Add rule",
3140
+ clear: "Clear rules",
3141
+ none: "No rules for this column",
3142
+ type: "Rule",
3143
+ style: "Format",
3144
+ wholeRow: "Whole row",
3145
+ count: "Count",
3146
+ types: { value: "Cell value", formula: "Formula", top: "Top", bottom: "Bottom", aboveAverage: "Above average", belowAverage: "Below average", duplicate: "Duplicate values", unique: "Unique values", dataBar: "Data bars", colorScale: "Color scale", iconSet: "Icon set" },
3147
+ ops: { notBetween: "Not between", notContains: "Does not contain" },
3148
+ styles: { red: "Light red fill", yellow: "Yellow fill", green: "Green fill", blue: "Blue fill", redText: "Red text", bold: "Bold" }
3149
+ },
2602
3150
  ops: {
2603
3151
  contains: "Contains",
2604
3152
  equals: "Equals",
@@ -2703,9 +3251,9 @@ function nextFocus(key, mods, p, s) {
2703
3251
  case "End":
2704
3252
  return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
2705
3253
  case "Tab": {
2706
- const flat = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
2707
- if (flat < 0 || flat >= s.rows * s.cols) return null;
2708
- return { rowIndex: Math.floor(flat / s.cols), colIndex: flat % s.cols };
3254
+ const flat2 = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
3255
+ if (flat2 < 0 || flat2 >= s.rows * s.cols) return null;
3256
+ return { rowIndex: Math.floor(flat2 / s.cols), colIndex: flat2 % s.cols };
2709
3257
  }
2710
3258
  default:
2711
3259
  return null;
@@ -2925,7 +3473,7 @@ function attachBody(grid) {
2925
3473
  const box = under?.getBoundingClientRect();
2926
3474
  let target = null;
2927
3475
  let position = "after";
2928
- let line;
3476
+ let line2;
2929
3477
  if (under && box) {
2930
3478
  const over = into.rowModel.getRow(Number(under.dataset.row));
2931
3479
  if (!over || over.kind === "group") return;
@@ -2934,17 +3482,17 @@ function attachBody(grid) {
2934
3482
  if (over.kind === "detail") position = "after";
2935
3483
  else if (into.options.treeData) position = y < 0.25 ? "before" : y > 0.75 ? "after" : "inside";
2936
3484
  else position = y < 0.5 ? "before" : "after";
2937
- line = position === "before" ? box.top : box.bottom;
3485
+ line2 = position === "before" ? box.top : box.bottom;
2938
3486
  } else {
2939
3487
  if (!point.closest(".fg-body")) return;
2940
3488
  const rows = intoRoot.querySelectorAll(".fg-body-pane-center .fg-row[data-row]");
2941
- line = rows.length ? Math.max(...[...rows].map((r) => r.getBoundingClientRect().bottom)) : intoRoot.querySelector(".fg-body").getBoundingClientRect().top;
3489
+ line2 = rows.length ? Math.max(...[...rows].map((r) => r.getBoundingClientRect().bottom)) : intoRoot.querySelector(".fg-body").getBoundingClientRect().top;
2942
3490
  }
2943
3491
  if (!into.canDropRow(row, target, position, grid)) return;
2944
3492
  drop = { into, target, position };
2945
3493
  const top = intoRoot.getBoundingClientRect().top;
2946
3494
  marker.classList.toggle("fg-row-drop-inside", position === "inside");
2947
- marker.style.top = `${position === "inside" ? box.top - top : line - top - 1}px`;
3495
+ marker.style.top = `${position === "inside" ? box.top - top : line2 - top - 1}px`;
2948
3496
  marker.style.height = position === "inside" ? `${box.height}px` : "";
2949
3497
  intoRoot.append(marker);
2950
3498
  };
@@ -3528,6 +4076,7 @@ var SideBar = class {
3528
4076
  this.grid.renderer.invalidate({ header: true, rows: true });
3529
4077
  }
3530
4078
  syncTabs() {
4079
+ this.el.classList.toggle("fg-sidebar-open", this.isOpen);
3531
4080
  for (const [p, tab] of this.tabs) {
3532
4081
  const on = this.isOpen && p === this.current;
3533
4082
  tab.setAttribute("aria-expanded", String(on));
@@ -3641,7 +4190,10 @@ var ContextMenu = class {
3641
4190
  items.push(
3642
4191
  "separator",
3643
4192
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
3644
- { label: loc.exportExcel, action: () => grid.api.exportXlsx() }
4193
+ { label: loc.exportExcel, action: () => grid.api.exportXlsx() },
4194
+ { label: loc.exportJson, action: () => grid.api.exportJson() },
4195
+ ...grid.options.assistant?.url ? [{ label: `${loc.ai.title}\u2026`, action: () => grid.api.openAssistant() }] : [],
4196
+ { label: loc.exportPdf, action: () => grid.api.exportPdf() }
3645
4197
  );
3646
4198
  if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
3647
4199
  items.push("separator", { label: loc.print, action: () => grid.api.print() });
@@ -3667,11 +4219,11 @@ var ContextMenu = class {
3667
4219
  value: node && col && !isSystemCol(colId) ? getCellValue(col.def, node.data) : void 0,
3668
4220
  defaultItems: defaults
3669
4221
  };
3670
- const custom = grid.options.contextMenu;
4222
+ const custom2 = grid.options.contextMenu;
3671
4223
  let items = defaults;
3672
- if (typeof custom === "function") {
4224
+ if (typeof custom2 === "function") {
3673
4225
  try {
3674
- items = custom(params);
4226
+ items = custom2(params);
3675
4227
  } catch {
3676
4228
  items = defaults;
3677
4229
  }
@@ -3931,10 +4483,10 @@ var Finder = class {
3931
4483
  const bar = el("div", "fg-findbar", { role: "search" });
3932
4484
  const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
3933
4485
  this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
3934
- const btn = (label, text2, run) => {
4486
+ const btn = (label, text2, run2) => {
3935
4487
  const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
3936
4488
  b.textContent = text2;
3937
- b.addEventListener("click", run);
4489
+ b.addEventListener("click", run2);
3938
4490
  return b;
3939
4491
  };
3940
4492
  bar.append(
@@ -3970,7 +4522,7 @@ var Finder = class {
3970
4522
  };
3971
4523
 
3972
4524
  // src/fill.ts
3973
- var DAY = 864e5;
4525
+ var DAY2 = 864e5;
3974
4526
  function fillSeries(source, count) {
3975
4527
  if (!source.length || count <= 0) return [];
3976
4528
  const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);
@@ -3982,9 +4534,9 @@ function fillSeries(source, count) {
3982
4534
  const last2 = nums[nums.length - 1];
3983
4535
  return Array.from({ length: count }, (_, i) => Math.round((last2 + step * (i + 1)) * 1e12) / 1e12);
3984
4536
  }
3985
- const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ? toDate(v) : null);
4537
+ const dates = source.map((v) => v instanceof Date || typeof v === "string" && /^\d{4}-\d{2}-\d{2}/.test(v) ? toDate2(v) : null);
3986
4538
  if (dates.every(Boolean) && source.length >= 2) {
3987
- const times = dates.map((d) => Math.round(d.getTime() / DAY));
4539
+ const times = dates.map((d) => Math.round(d.getTime() / DAY2));
3988
4540
  const step = times[1] - times[0];
3989
4541
  if (times.every((t, i) => i === 0 || t - times[i - 1] === step)) {
3990
4542
  const lastDate = dates[dates.length - 1];
@@ -4119,7 +4671,7 @@ function parseDate(text2, previous) {
4119
4671
  let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
4120
4672
  if (m) date = validDate(+m[1], +m[2], +m[3]);
4121
4673
  else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
4122
- else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate(s);
4674
+ else if (/^\d{4}-\d{2}-\d{2}T/.test(s)) date = toDate2(s);
4123
4675
  if (!date) return { ok: false };
4124
4676
  return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
4125
4677
  }
@@ -4143,16 +4695,16 @@ function parseValue(def, text2, previous) {
4143
4695
 
4144
4696
  // src/editing/editor.ts
4145
4697
  var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false });
4146
- var pad2 = (n) => String(n).padStart(2, "0");
4698
+ var pad3 = (n) => String(n).padStart(2, "0");
4147
4699
  function localDateTime(v) {
4148
4700
  const d = v instanceof Date ? v : typeof v === "string" && v ? new Date(v) : null;
4149
4701
  if (!d || Number.isNaN(d.getTime())) return "";
4150
- return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}`;
4702
+ return `${d.getFullYear()}-${pad3(d.getMonth() + 1)}-${pad3(d.getDate())}T${pad3(d.getHours())}:${pad3(d.getMinutes())}`;
4151
4703
  }
4152
4704
  function timeOf(v) {
4153
- if (v instanceof Date) return `${pad2(v.getHours())}:${pad2(v.getMinutes())}`;
4705
+ if (v instanceof Date) return `${pad3(v.getHours())}:${pad3(v.getMinutes())}`;
4154
4706
  const m = typeof v === "string" ? /(\d{1,2}):(\d{2})/.exec(v) : null;
4155
- return m ? `${pad2(Number(m[1]))}:${m[2]}` : "";
4707
+ return m ? `${pad3(Number(m[1]))}:${m[2]}` : "";
4156
4708
  }
4157
4709
  var CellEditor = class {
4158
4710
  constructor(grid) {
@@ -4227,20 +4779,20 @@ var CellEditor = class {
4227
4779
  grid.setFocus(rowIndex, colId);
4228
4780
  renderer.ensureVisible(rowIndex, colId);
4229
4781
  renderer.render();
4230
- const wrap = el("div", "fg-editor");
4231
- this.place(wrap, rowIndex, colId);
4782
+ const wrap2 = el("div", "fg-editor");
4783
+ this.place(wrap2, rowIndex, colId);
4232
4784
  let control;
4233
4785
  let read;
4234
4786
  let dispose = () => {
4235
4787
  };
4236
4788
  const spec = def.editor;
4237
4789
  if (typeof spec === "function") {
4238
- const custom = spec({ value, row, column: def, initialText });
4239
- control = custom.el;
4240
- read = () => ({ value: custom.getValue() });
4241
- dispose = () => custom.destroy?.();
4242
- wrap.append(control);
4243
- queueMicrotask(() => custom.focus?.());
4790
+ const custom2 = spec({ value, row, column: def, initialText });
4791
+ control = custom2.el;
4792
+ read = () => ({ value: custom2.getValue() });
4793
+ dispose = () => custom2.destroy?.();
4794
+ wrap2.append(control);
4795
+ queueMicrotask(() => custom2.focus?.());
4244
4796
  } else if (spec?.type === "select") {
4245
4797
  const options = normalizeOptions(typeof spec.options === "function" ? spec.options(row) : spec.options);
4246
4798
  const select = el("select", "fg-editor-control");
@@ -4254,7 +4806,7 @@ var CellEditor = class {
4254
4806
  select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
4255
4807
  control = select;
4256
4808
  read = () => ({ value: options[select.selectedIndex]?.value ?? null });
4257
- wrap.append(select);
4809
+ wrap2.append(select);
4258
4810
  } else if (spec?.type === "time" || spec?.type === "datetime" || !spec && def.type === "date" && def.format === "datetime") {
4259
4811
  const isTime = spec?.type === "time";
4260
4812
  const input = el("input", "fg-editor-control", { type: isTime ? "time" : "datetime-local" });
@@ -4265,7 +4817,7 @@ var CellEditor = class {
4265
4817
  if (isTime) return { value: input.value };
4266
4818
  return { value: value instanceof Date ? new Date(input.value) : input.value };
4267
4819
  };
4268
- wrap.append(input);
4820
+ wrap2.append(input);
4269
4821
  } else {
4270
4822
  const textarea = spec?.type === "textarea";
4271
4823
  const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
@@ -4277,11 +4829,11 @@ var CellEditor = class {
4277
4829
  input.value = initialText ?? start;
4278
4830
  control = input;
4279
4831
  read = () => ({ text: input.value });
4280
- wrap.append(input);
4832
+ wrap2.append(input);
4281
4833
  }
4282
4834
  control.setAttribute("aria-label", def.header ?? def.field ?? colId);
4283
4835
  const msg = el("div", "fg-editor-msg", { role: "alert" });
4284
- wrap.append(msg);
4836
+ wrap2.append(msg);
4285
4837
  const onKey = (e) => {
4286
4838
  e.stopPropagation();
4287
4839
  if (e.key === "Escape") {
@@ -4297,26 +4849,26 @@ var CellEditor = class {
4297
4849
  }
4298
4850
  };
4299
4851
  const onFocusOut = (e) => {
4300
- if (this.cur?.wrap === wrap && !wrap.contains(e.relatedTarget)) this.commit(null);
4852
+ if (this.cur?.wrap === wrap2 && !wrap2.contains(e.relatedTarget)) this.commit(null);
4301
4853
  };
4302
4854
  const onScroll = () => this.commit(null);
4303
- wrap.addEventListener("keydown", onKey);
4304
- wrap.addEventListener("focusout", onFocusOut);
4855
+ wrap2.addEventListener("keydown", onKey);
4856
+ wrap2.addEventListener("focusout", onFocusOut);
4305
4857
  renderer.scroller.addEventListener("scroll", onScroll, { passive: true });
4306
- renderer.root.append(wrap);
4858
+ renderer.root.append(wrap2);
4307
4859
  this.cur = {
4308
4860
  rowIndex,
4309
4861
  colId,
4310
4862
  row,
4311
4863
  def,
4312
- wrap,
4864
+ wrap: wrap2,
4313
4865
  read,
4314
4866
  dispose: () => {
4315
- wrap.removeEventListener("keydown", onKey);
4316
- wrap.removeEventListener("focusout", onFocusOut);
4867
+ wrap2.removeEventListener("keydown", onKey);
4868
+ wrap2.removeEventListener("focusout", onFocusOut);
4317
4869
  renderer.scroller.removeEventListener("scroll", onScroll);
4318
4870
  dispose();
4319
- wrap.remove();
4871
+ wrap2.remove();
4320
4872
  }
4321
4873
  };
4322
4874
  if (typeof spec !== "function") {
@@ -4327,15 +4879,15 @@ var CellEditor = class {
4327
4879
  return true;
4328
4880
  }
4329
4881
  /** Put the editor over its cell; false when the cell is not rendered. */
4330
- place(wrap, rowIndex, colId) {
4882
+ place(wrap2, rowIndex, colId) {
4331
4883
  const { renderer } = this.grid;
4332
4884
  const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
4333
4885
  const rootBox = renderer.root.getBoundingClientRect();
4334
4886
  const box = cellEl?.getBoundingClientRect();
4335
- wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4336
- wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4337
- wrap.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
4338
- wrap.style.height = `${box?.height || renderer.opts.rowHeight}px`;
4887
+ wrap2.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4888
+ wrap2.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4889
+ wrap2.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
4890
+ wrap2.style.height = `${box?.height || renderer.opts.rowHeight}px`;
4339
4891
  return !!cellEl;
4340
4892
  }
4341
4893
  /** Display index of a row object, or -1 when it is no longer shown. */
@@ -4679,6 +5231,610 @@ function attachHeader(grid) {
4679
5231
  };
4680
5232
  }
4681
5233
 
5234
+ // src/export/pdf.ts
5235
+ var PAPER_MM = {
5236
+ A4: [210, 297],
5237
+ A3: [297, 420],
5238
+ A5: [148, 210],
5239
+ B4: [250, 353],
5240
+ B5: [176, 250],
5241
+ letter: [215.9, 279.4],
5242
+ legal: [215.9, 355.6]
5243
+ };
5244
+ var PX_PER_MM = 96 / 25.4;
5245
+ var PT_PER_MM = 72 / 25.4;
5246
+ var FONT = "system-ui, 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', 'Noto Sans CJK KR', sans-serif";
5247
+ var FS = 10.5;
5248
+ var LINE = 15;
5249
+ var PAD_X = 7;
5250
+ var PAD_Y = 4;
5251
+ var MAX_LINES = 8;
5252
+ function base64Bytes(b64) {
5253
+ const bin = atob(b64);
5254
+ const out = new Uint8Array(bin.length);
5255
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
5256
+ return out;
5257
+ }
5258
+ var browserSurface = {
5259
+ surface(width, height) {
5260
+ if (typeof document === "undefined") return null;
5261
+ const canvas = document.createElement("canvas");
5262
+ canvas.width = width;
5263
+ canvas.height = height;
5264
+ let ctx = null;
5265
+ try {
5266
+ ctx = canvas.getContext("2d");
5267
+ } catch {
5268
+ ctx = null;
5269
+ }
5270
+ if (!ctx) return null;
5271
+ return {
5272
+ ctx,
5273
+ jpeg: (q) => base64Bytes(canvas.toDataURL("image/jpeg", q).split(",")[1] ?? "")
5274
+ };
5275
+ }
5276
+ };
5277
+ var fontOf = (weight, size = FS) => `${weight} ${size}px ${FONT}`;
5278
+ function wrap(ctx, text2, width) {
5279
+ if (!text2) return [""];
5280
+ const out = [];
5281
+ for (const para of text2.split(/\r?\n/)) {
5282
+ let line2 = "";
5283
+ const parts = para.match(/[ -鿿가-힯＀-￯]|[^\s -鿿가-힯＀-￯]+\s*|\s+/g) ?? [""];
5284
+ for (const part of parts) {
5285
+ const next = line2 + part;
5286
+ if (!line2 || ctx.measureText(next.trimEnd()).width <= width) {
5287
+ if (!line2 && ctx.measureText(part.trimEnd()).width > width) {
5288
+ for (const ch of part) {
5289
+ if (line2 && ctx.measureText(line2 + ch).width > width) {
5290
+ out.push(line2);
5291
+ line2 = "";
5292
+ }
5293
+ line2 += ch;
5294
+ }
5295
+ } else line2 = next;
5296
+ } else {
5297
+ out.push(line2.trimEnd());
5298
+ line2 = part.trimStart();
5299
+ }
5300
+ if (out.length >= MAX_LINES) break;
5301
+ }
5302
+ out.push(line2.trimEnd());
5303
+ if (out.length >= MAX_LINES) break;
5304
+ }
5305
+ if (out.length > MAX_LINES) out.length = MAX_LINES;
5306
+ return out;
5307
+ }
5308
+ function geometry(m) {
5309
+ const [w, h] = PAPER_MM[m.paper] ?? PAPER_MM.A4;
5310
+ const [pw, ph] = m.orientation === "landscape" ? [h, w] : [w, h];
5311
+ const pageW = pw * PX_PER_MM, pageH = ph * PX_PER_MM;
5312
+ const margin = 12 * PX_PER_MM, marginBottom = 14 * PX_PER_MM;
5313
+ const contentW = pageW - margin * 2;
5314
+ const fixed = m.numbered ? 40 : 0;
5315
+ const data = m.widths.slice(m.numbered ? 1 : 0);
5316
+ const sum = data.reduce((a, b) => a + b, 0) || 1;
5317
+ const scale = m.fit ? (contentW - fixed) / sum : Math.min(1, (contentW - fixed) / sum);
5318
+ const colW = [...m.numbered ? [fixed] : [], ...data.map((x2) => x2 * scale)];
5319
+ const colX = [];
5320
+ let x = margin;
5321
+ for (const cw of colW) {
5322
+ colX.push(x);
5323
+ x += cw;
5324
+ }
5325
+ return { pageW, pageH, margin, marginBottom, colX, colW };
5326
+ }
5327
+ function placeLine(ctx, g, cells, kind, level) {
5328
+ const placed = { cells: [], kind, level, y: 0, h: 0 };
5329
+ let col = 0, h = 22;
5330
+ for (const cell of cells) {
5331
+ const span = Math.max(1, cell.span ?? 1);
5332
+ const x = g.colX[col] ?? g.margin;
5333
+ let w = 0;
5334
+ for (let k = col; k < col + span && k < g.colW.length; k++) w += g.colW[k];
5335
+ col += span;
5336
+ ctx.font = fontOf(kind === "group" ? 600 : kind === "total" || cell.bold ? 700 : 400);
5337
+ const indent = (cell.indent ?? 0) * 14;
5338
+ const lines = wrap(ctx, cell.text, Math.max(8, w - PAD_X * 2 - indent - (cell.icon ? 14 : 0)));
5339
+ h = Math.max(h, lines.length * LINE + PAD_Y * 2);
5340
+ placed.cells.push({ cell, x, w, y: 0, h: 0, lines });
5341
+ }
5342
+ placed.h = h;
5343
+ return placed;
5344
+ }
5345
+ function placeHead(ctx, g, head) {
5346
+ const rows = head.map(() => ({ cells: [], kind: "head", y: 0, h: 22 }));
5347
+ const taken = head.map(() => g.colW.map(() => false));
5348
+ const items = [];
5349
+ ctx.font = fontOf(600);
5350
+ head.forEach((tr, r) => {
5351
+ let col = 0;
5352
+ for (const cell of tr) {
5353
+ while (col < g.colW.length && taken[r][col]) col++;
5354
+ const span = Math.max(1, cell.span ?? 1), rs = Math.max(1, Math.min(head.length - r, cell.rowspan ?? 1));
5355
+ for (let a = r; a < r + rs; a++) for (let b = col; b < col + span && b < g.colW.length; b++) taken[a][b] = true;
5356
+ let w = 0;
5357
+ for (let k = col; k < col + span && k < g.colW.length; k++) w += g.colW[k];
5358
+ const lines = wrap(ctx, cell.text, Math.max(8, w - PAD_X * 2));
5359
+ items.push({ cell, r, col, span, rs, lines, x: g.colX[col] ?? g.margin, w });
5360
+ col += span;
5361
+ }
5362
+ });
5363
+ for (const it of items) {
5364
+ const need = it.lines.length * LINE + PAD_Y * 2;
5365
+ const each = need / it.rs;
5366
+ for (let a = it.r; a < it.r + it.rs; a++) rows[a].h = Math.max(rows[a].h, each);
5367
+ }
5368
+ for (const it of items) {
5369
+ rows[it.r].cells.push({ cell: it.cell, x: it.x, w: it.w, y: 0, h: rows.slice(it.r, it.r + it.rs).reduce((s, x) => s + x.h, 0), lines: it.lines });
5370
+ }
5371
+ return rows;
5372
+ }
5373
+ function titleHeight(m) {
5374
+ if (!m.title && !m.subtitle && !m.printed) return 0;
5375
+ return (m.title ? 24 : 12) + (m.subtitle ? 17 : 0) + 18;
5376
+ }
5377
+ function paginate(ctx, m) {
5378
+ const g = geometry(m);
5379
+ const head = placeHead(ctx, g, m.head);
5380
+ let n = 0;
5381
+ const lines = m.body.map((l) => {
5382
+ const p = placeLine(ctx, g, l.cells, l.kind, l.level);
5383
+ if (l.kind === "row") p.odd = n++ % 2 === 1;
5384
+ return p;
5385
+ });
5386
+ const repeated = lines.slice(0, m.repeatRows);
5387
+ const rest = lines.slice(m.repeatRows);
5388
+ const foot = m.foot.map((l) => placeLine(ctx, g, l.cells, l.kind, l.level));
5389
+ const bottom = g.pageH - g.marginBottom;
5390
+ const tH = titleHeight(m);
5391
+ const pages = [];
5392
+ const queue = [...rest, ...foot];
5393
+ const clone = (p) => ({ ...p, cells: p.cells.map((c) => ({ ...c })) });
5394
+ let i = 0;
5395
+ do {
5396
+ const first = pages.length === 0;
5397
+ const page = { rows: [], title: first || m.repeatTitle };
5398
+ let y = g.margin + (page.title ? tH : 0);
5399
+ const put = (p) => {
5400
+ const c = clone(p);
5401
+ c.y = y;
5402
+ for (const cell of c.cells) {
5403
+ cell.y = y;
5404
+ if (c.kind !== "head") cell.h = c.h;
5405
+ }
5406
+ page.rows.push(c);
5407
+ y += c.h;
5408
+ };
5409
+ if (first || m.repeatHeader) head.forEach(put);
5410
+ repeated.forEach(put);
5411
+ const top = y;
5412
+ while (i < queue.length) {
5413
+ const p = queue[i];
5414
+ const need = p.kind === "group" && i + 1 < queue.length ? p.h + queue[i + 1].h : p.h;
5415
+ if (y + need > bottom && y > top) break;
5416
+ put(p);
5417
+ i++;
5418
+ }
5419
+ pages.push(page);
5420
+ } while (i < queue.length && pages.length < 5e3);
5421
+ return { pages, g };
5422
+ }
5423
+ function line(ctx, x1, y1, x2, y2, color, width) {
5424
+ ctx.strokeStyle = color;
5425
+ ctx.lineWidth = width;
5426
+ ctx.beginPath();
5427
+ ctx.moveTo(x1, y1);
5428
+ ctx.lineTo(x2, y2);
5429
+ ctx.stroke();
5430
+ }
5431
+ function paintRow(ctx, row, m, g) {
5432
+ const right = g.colX.length ? g.colX[g.colX.length - 1] + g.colW[g.colW.length - 1] : g.pageW - g.margin;
5433
+ const rowFill = row.kind === "head" ? "#f3f4f6" : row.kind === "group" ? row.level ? "#eef2f7" : "#e2e8f0" : row.kind === "total" ? "#f3f4f6" : m.striped && row.odd ? "#f9fafb" : "";
5434
+ for (const c of row.cells) {
5435
+ const fill = c.cell.fill || rowFill;
5436
+ if (fill) {
5437
+ ctx.fillStyle = fill;
5438
+ ctx.fillRect(c.x, c.y, c.w, c.h);
5439
+ }
5440
+ if (c.cell.bar) {
5441
+ ctx.save();
5442
+ ctx.globalAlpha = 0.4;
5443
+ ctx.fillStyle = c.cell.bar.color;
5444
+ ctx.fillRect(c.x + c.w * c.cell.bar.from, c.y + c.h * 0.15, c.w * (c.cell.bar.to - c.cell.bar.from), c.h * 0.7);
5445
+ ctx.restore();
5446
+ }
5447
+ const weight = row.kind === "head" || row.kind === "group" ? 600 : row.kind === "total" ? 700 : c.cell.bold ? 600 : 400;
5448
+ ctx.font = `${c.cell.italic ? "italic " : ""}${fontOf(weight)}`;
5449
+ ctx.fillStyle = c.cell.num ? "#9ca3af" : c.cell.color || (row.kind === "head" ? "#374151" : "#111827");
5450
+ ctx.textBaseline = "middle";
5451
+ ctx.save();
5452
+ ctx.beginPath();
5453
+ ctx.rect(c.x, c.y, c.w, c.h);
5454
+ ctx.clip();
5455
+ const indent = (c.cell.indent ?? 0) * 14;
5456
+ const textH = c.lines.length * LINE;
5457
+ let ty = row.kind === "head" ? c.y + c.h - PAD_Y - textH + LINE / 2 : c.y + PAD_Y + LINE / 2;
5458
+ if (row.kind !== "head" && c.h > textH + PAD_Y * 2) ty = c.y + PAD_Y + LINE / 2;
5459
+ for (const [k, text2] of c.lines.entries()) {
5460
+ let x = c.x + PAD_X + indent;
5461
+ const wText = ctx.measureText(text2).width + (c.cell.icon && k === 0 ? 14 : 0);
5462
+ const align = c.cell.group ? "center" : c.cell.align;
5463
+ if (align === "right") x = c.x + c.w - PAD_X - wText;
5464
+ else if (align === "center") x = c.x + (c.w - wText) / 2;
5465
+ ctx.textAlign = "left";
5466
+ if (c.cell.icon && k === 0) {
5467
+ const keep = ctx.fillStyle;
5468
+ ctx.fillStyle = c.cell.icon.color;
5469
+ ctx.fillText(c.cell.icon.char, x, ty);
5470
+ ctx.fillStyle = keep;
5471
+ x += 14;
5472
+ }
5473
+ ctx.fillText(text2, x, ty);
5474
+ if (c.cell.strike) line(ctx, x, ty, x + ctx.measureText(text2).width, ty, String(ctx.fillStyle), 1);
5475
+ ty += LINE;
5476
+ }
5477
+ ctx.restore();
5478
+ if (row.kind === "head" && c.x > g.margin + 0.5) line(ctx, c.x, c.y, c.x, c.y + c.h, "#e5e7eb", 1);
5479
+ if (row.kind === "head") line(ctx, c.x, c.y + c.h, c.x + c.w, c.y + c.h, "#9ca3af", 1.5);
5480
+ }
5481
+ if (row.kind === "group") line(ctx, g.margin, row.y, right, row.y, "#cbd5e1", 1);
5482
+ if (row.kind === "total") line(ctx, g.margin, row.y, right, row.y, "#111827", 1.5);
5483
+ else if (row.kind !== "head") line(ctx, g.margin, row.y + row.h, right, row.y + row.h, "#e5e7eb", 1);
5484
+ }
5485
+ function paintTitle(ctx, m, g) {
5486
+ const h = titleHeight(m);
5487
+ if (!h) return;
5488
+ const right = g.pageW - g.margin;
5489
+ let y = g.margin;
5490
+ ctx.textAlign = "left";
5491
+ ctx.textBaseline = "top";
5492
+ if (m.title) {
5493
+ ctx.font = fontOf(700, 17);
5494
+ ctx.fillStyle = "#111827";
5495
+ ctx.fillText(m.title, g.margin, y);
5496
+ y += 24;
5497
+ }
5498
+ if (m.subtitle) {
5499
+ ctx.font = fontOf(400, 11);
5500
+ ctx.fillStyle = "#4b5563";
5501
+ ctx.fillText(m.subtitle, g.margin, y);
5502
+ y += 17;
5503
+ }
5504
+ if (m.printed) {
5505
+ ctx.font = fontOf(400, 9.5);
5506
+ ctx.fillStyle = "#6b7280";
5507
+ const w = ctx.measureText(m.printed).width;
5508
+ ctx.fillText(m.printed, right - w, g.margin + h - 18 - 13);
5509
+ }
5510
+ line(ctx, g.margin, g.margin + h - 10, right, g.margin + h - 10, "#111827", 2);
5511
+ }
5512
+ function paintFooter(ctx, m, g, index, total) {
5513
+ const y = g.pageH - 7 * PX_PER_MM;
5514
+ ctx.font = fontOf(400, 8.5);
5515
+ ctx.fillStyle = "#6b7280";
5516
+ ctx.textBaseline = "middle";
5517
+ ctx.textAlign = "left";
5518
+ if (m.footer) ctx.fillText(m.footer, g.margin, y);
5519
+ if (m.pageNumbers) {
5520
+ const t = `${index + 1} / ${total}`;
5521
+ ctx.fillText(t, g.pageW - g.margin - ctx.measureText(t).width, y);
5522
+ }
5523
+ }
5524
+ function renderPdf(m, deps = browserSurface, opts = {}) {
5525
+ const scale = typeof opts.scale === "number" && opts.scale > 0 ? Math.min(4, opts.scale) : 2;
5526
+ const quality = typeof opts.quality === "number" && opts.quality > 0 && opts.quality <= 1 ? opts.quality : 0.92;
5527
+ const probe = deps.surface(8, 8);
5528
+ if (!probe) return null;
5529
+ const { pages, g } = paginate(probe.ctx, m);
5530
+ const wPx = Math.round(g.pageW * scale), hPx = Math.round(g.pageH * scale);
5531
+ const images = [];
5532
+ pages.forEach((page, i) => {
5533
+ const s = deps.surface(wPx, hPx);
5534
+ if (!s) return;
5535
+ const ctx = s.ctx;
5536
+ ctx.setTransform(scale, 0, 0, scale, 0, 0);
5537
+ ctx.fillStyle = "#ffffff";
5538
+ ctx.fillRect(0, 0, g.pageW, g.pageH);
5539
+ if (page.title) paintTitle(ctx, m, g);
5540
+ for (const row of page.rows) paintRow(ctx, row, m, g);
5541
+ paintFooter(ctx, m, g, i, pages.length);
5542
+ images.push(s.jpeg(quality));
5543
+ });
5544
+ const [mw, mh] = PAPER_MM[m.paper] ?? PAPER_MM.A4;
5545
+ const [pw, ph] = m.orientation === "landscape" ? [mh, mw] : [mw, mh];
5546
+ return writePdf(images.map((jpeg) => ({ jpeg, width: wPx, height: hPx })), pw * PT_PER_MM, ph * PT_PER_MM, m.title);
5547
+ }
5548
+ function pdfString(s) {
5549
+ if (/^[\x20-\x7e]*$/.test(s)) return `(${s.replace(/[\\()]/g, (c) => `\\${c}`)})`;
5550
+ let hex2 = "FEFF";
5551
+ for (let i = 0; i < s.length; i++) hex2 += s.charCodeAt(i).toString(16).padStart(4, "0").toUpperCase();
5552
+ return `<${hex2}>`;
5553
+ }
5554
+ function writePdf(pages, ptW, ptH, title = "") {
5555
+ const enc = new TextEncoder();
5556
+ const parts = [];
5557
+ const offsets = [];
5558
+ let size = 0;
5559
+ const push = (b) => {
5560
+ const bytes = typeof b === "string" ? enc.encode(b) : b;
5561
+ parts.push(bytes);
5562
+ size += bytes.length;
5563
+ };
5564
+ const obj = (n2, body) => {
5565
+ offsets[n2] = size;
5566
+ push(`${n2} 0 obj
5567
+ `);
5568
+ if (typeof body === "string") push(body);
5569
+ else body();
5570
+ push("\nendobj\n");
5571
+ };
5572
+ push("%PDF-1.4\n");
5573
+ push(new Uint8Array([37, 226, 227, 207, 211, 10]));
5574
+ const n = pages.length;
5575
+ const pageIds = pages.map((_, i) => 4 + i * 3);
5576
+ const w = ptW.toFixed(2), h = ptH.toFixed(2);
5577
+ obj(1, "<< /Type /Catalog /Pages 2 0 R >>");
5578
+ obj(2, `<< /Type /Pages /Count ${n} /Kids [${pageIds.map((id) => `${id} 0 R`).join(" ")}] >>`);
5579
+ obj(3, `<< /Producer (FreeGrid)${title ? ` /Title ${pdfString(title)}` : ""} >>`);
5580
+ pages.forEach((p, i) => {
5581
+ const id = pageIds[i];
5582
+ const content = `q ${w} 0 0 ${h} 0 0 cm /Im0 Do Q`;
5583
+ obj(id, `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${w} ${h}] /Resources << /XObject << /Im0 ${id + 2} 0 R >> >> /Contents ${id + 1} 0 R >>`);
5584
+ obj(id + 1, `<< /Length ${content.length} >>
5585
+ stream
5586
+ ${content}
5587
+ endstream`);
5588
+ obj(id + 2, () => {
5589
+ push(`<< /Type /XObject /Subtype /Image /Width ${p.width} /Height ${p.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length ${p.jpeg.length} >>
5590
+ stream
5591
+ `);
5592
+ push(p.jpeg);
5593
+ push("\nendstream");
5594
+ });
5595
+ });
5596
+ const count = 4 + n * 3;
5597
+ const xref = size;
5598
+ let table = `xref
5599
+ 0 ${count}
5600
+ 0000000000 65535 f
5601
+ `;
5602
+ for (let k = 1; k < count; k++) table += `${String(offsets[k] ?? 0).padStart(10, "0")} 00000 n
5603
+ `;
5604
+ push(`${table}trailer
5605
+ << /Size ${count} /Root 1 0 R /Info 3 0 R >>
5606
+ startxref
5607
+ ${xref}
5608
+ %%EOF
5609
+ `);
5610
+ const out = new Uint8Array(size);
5611
+ let at = 0;
5612
+ for (const part of parts) {
5613
+ out.set(part, at);
5614
+ at += part.length;
5615
+ }
5616
+ return out;
5617
+ }
5618
+
5619
+ // src/conditional.ts
5620
+ var NUMERIC = (v) => {
5621
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
5622
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
5623
+ if (v instanceof Date) return v.getTime();
5624
+ return null;
5625
+ };
5626
+ var STAT_TYPES = /* @__PURE__ */ new Set(["top", "bottom", "aboveAverage", "belowAverage", "duplicate", "unique", "dataBar", "colorScale", "iconSet"]);
5627
+ var ICON_SETS = {
5628
+ // high, middle, low
5629
+ arrows: [{ char: "\u25B2", color: "#16a34a", label: "up" }, { char: "\u25B6", color: "#d97706", label: "level" }, { char: "\u25BC", color: "#dc2626", label: "down" }],
5630
+ traffic: [{ char: "\u25CF", color: "#16a34a", label: "good" }, { char: "\u25CF", color: "#f59e0b", label: "fair" }, { char: "\u25CF", color: "#dc2626", label: "poor" }],
5631
+ signs: [{ char: "\u2714", color: "#16a34a", label: "good" }, { char: "!", color: "#d97706", label: "fair" }, { char: "\u2716", color: "#dc2626", label: "poor" }]
5632
+ };
5633
+ function hex(c) {
5634
+ const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(c.trim());
5635
+ if (!m) return null;
5636
+ const h = m[1].length === 3 ? [...m[1]].map((x) => x + x).join("") : m[1];
5637
+ return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
5638
+ }
5639
+ function mixColor(a, b, t) {
5640
+ const x = hex(a), y = hex(b);
5641
+ if (!x || !y) return t < 0.5 ? a : b;
5642
+ const c = x.map((v, i) => Math.round(v + (y[i] - v) * Math.max(0, Math.min(1, t))));
5643
+ return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
5644
+ }
5645
+ function textOf(v) {
5646
+ return v == null ? "" : v instanceof Date ? v.toISOString() : String(v);
5647
+ }
5648
+ function valueMatches(rule, value) {
5649
+ const op = rule.op ?? "eq";
5650
+ if (op === "blank") return isBlank(value);
5651
+ if (op === "notBlank") return !isBlank(value);
5652
+ if (op === "contains" || op === "notContains" || op === "startsWith" || op === "endsWith") {
5653
+ const s2 = textOf(value).toLowerCase(), q = textOf(rule.value).toLowerCase();
5654
+ const hit = op === "startsWith" ? s2.startsWith(q) : op === "endsWith" ? s2.endsWith(q) : s2.includes(q);
5655
+ return op === "notContains" ? !hit : hit;
5656
+ }
5657
+ if (isBlank(value)) return op === "ne";
5658
+ const a = NUMERIC(value), b = NUMERIC(rule.value), c = NUMERIC(rule.valueTo);
5659
+ if (a !== null && b !== null) {
5660
+ switch (op) {
5661
+ case "eq":
5662
+ return a === b;
5663
+ case "ne":
5664
+ return a !== b;
5665
+ case "gt":
5666
+ return a > b;
5667
+ case "gte":
5668
+ return a >= b;
5669
+ case "lt":
5670
+ return a < b;
5671
+ case "lte":
5672
+ return a <= b;
5673
+ case "between":
5674
+ case "notBetween": {
5675
+ if (c === null) return false;
5676
+ const inside = a >= Math.min(b, c) && a <= Math.max(b, c);
5677
+ return op === "between" ? inside : !inside;
5678
+ }
5679
+ }
5680
+ }
5681
+ const s = textOf(value).toLowerCase(), t = textOf(rule.value).toLowerCase(), u = textOf(rule.valueTo).toLowerCase();
5682
+ switch (op) {
5683
+ case "eq":
5684
+ return s === t;
5685
+ case "ne":
5686
+ return s !== t;
5687
+ case "gt":
5688
+ return s > t;
5689
+ case "gte":
5690
+ return s >= t;
5691
+ case "lt":
5692
+ return s < t;
5693
+ case "lte":
5694
+ return s <= t;
5695
+ case "between":
5696
+ return s >= t && s <= u;
5697
+ case "notBetween":
5698
+ return s < t || s > u;
5699
+ }
5700
+ return false;
5701
+ }
5702
+ var ConditionalFormatter = class {
5703
+ constructor(deps) {
5704
+ this.deps = deps;
5705
+ this.stats = /* @__PURE__ */ new Map();
5706
+ this.statsKey = [];
5707
+ this.rowCache = /* @__PURE__ */ new WeakMap();
5708
+ }
5709
+ /** True when any rule looks at other rows (an update to one row can restyle others). */
5710
+ dependsOnOtherRows() {
5711
+ return this.deps.rules().some((r) => r && (STAT_TYPES.has(r.type ?? "value") || r.type === "formula"));
5712
+ }
5713
+ fresh() {
5714
+ const key = [this.deps.rows(), this.deps.version(), this.deps.rules()];
5715
+ if (key.every((k, i) => k === this.statsKey[i])) return;
5716
+ this.statsKey = key;
5717
+ this.stats.clear();
5718
+ this.rowCache = /* @__PURE__ */ new WeakMap();
5719
+ }
5720
+ statsOf(colId) {
5721
+ let s = this.stats.get(colId);
5722
+ if (!s) {
5723
+ const nums = [];
5724
+ const counts = /* @__PURE__ */ new Map();
5725
+ for (const row of this.deps.rows()) {
5726
+ const v = this.deps.value(colId, row);
5727
+ const n = NUMERIC(v);
5728
+ if (n !== null) nums.push(n);
5729
+ if (!isBlank(v)) {
5730
+ const k = setKey(v);
5731
+ counts.set(k, (counts.get(k) ?? 0) + 1);
5732
+ }
5733
+ }
5734
+ nums.sort((a, b) => a - b);
5735
+ const sum = nums.reduce((a, b) => a + b, 0);
5736
+ s = { min: nums[0] ?? 0, max: nums[nums.length - 1] ?? 0, avg: nums.length ? sum / nums.length : 0, sorted: nums, counts };
5737
+ this.stats.set(colId, s);
5738
+ }
5739
+ return s;
5740
+ }
5741
+ test(rule, colId, value, row) {
5742
+ const type = rule.type ?? "value";
5743
+ switch (type) {
5744
+ case "value":
5745
+ return valueMatches(rule, value);
5746
+ case "formula": {
5747
+ if (typeof rule.formula !== "string") return false;
5748
+ const r = this.deps.formula(rule.formula.startsWith("=") ? rule.formula : `=${rule.formula}`, row);
5749
+ return r === true || typeof r === "number" && r !== 0;
5750
+ }
5751
+ case "top":
5752
+ case "bottom": {
5753
+ const n = NUMERIC(value);
5754
+ if (n === null) return false;
5755
+ const s = this.statsOf(colId);
5756
+ if (!s.sorted.length) return false;
5757
+ const k = Math.max(1, rule.percent ? Math.ceil(s.sorted.length * (rule.rank ?? 10) / 100) : Math.floor(rule.rank ?? 10));
5758
+ return type === "top" ? n >= s.sorted[Math.max(0, s.sorted.length - k)] : n <= s.sorted[Math.min(s.sorted.length - 1, k - 1)];
5759
+ }
5760
+ case "aboveAverage":
5761
+ case "belowAverage": {
5762
+ const n = NUMERIC(value);
5763
+ if (n === null) return false;
5764
+ const avg = this.statsOf(colId).avg;
5765
+ return type === "aboveAverage" ? n > avg : n < avg;
5766
+ }
5767
+ case "duplicate":
5768
+ case "unique": {
5769
+ if (isBlank(value)) return false;
5770
+ const c = this.statsOf(colId).counts.get(setKey(value)) ?? 0;
5771
+ return type === "duplicate" ? c > 1 : c === 1;
5772
+ }
5773
+ }
5774
+ return false;
5775
+ }
5776
+ /** Style from rules with `row: true`, for every cell of the row. */
5777
+ rowStyle(row) {
5778
+ if (this.rowCache.has(row)) return this.rowCache.get(row);
5779
+ let style = null;
5780
+ for (const rule of this.deps.rules()) {
5781
+ if (!rule?.row || !Array.isArray(rule.columns) || !rule.columns.length) continue;
5782
+ const colId = rule.columns[0];
5783
+ if (this.test(rule, colId, this.deps.value(colId, row), row)) {
5784
+ style = { ...style ?? {}, ...rule.style };
5785
+ if (rule.stopIfTrue) break;
5786
+ }
5787
+ }
5788
+ this.rowCache.set(row, style);
5789
+ return style;
5790
+ }
5791
+ /** Format of one cell; null when no rule applies. */
5792
+ cell(colId, row, value) {
5793
+ const rules = this.deps.rules();
5794
+ if (!rules.length) return null;
5795
+ this.fresh();
5796
+ let out = null;
5797
+ const rs = this.rowStyle(row);
5798
+ if (rs) out = { style: { ...rs } };
5799
+ for (const rule of rules) {
5800
+ if (!rule || rule.row || !Array.isArray(rule.columns) || !rule.columns.includes(colId)) continue;
5801
+ const type = rule.type ?? "value";
5802
+ if (type === "dataBar" || type === "colorScale" || type === "iconSet") {
5803
+ const n = NUMERIC(value);
5804
+ if (n === null) continue;
5805
+ const s = this.statsOf(colId);
5806
+ const lo = typeof rule.min === "number" ? rule.min : s.min;
5807
+ const hi = typeof rule.max === "number" ? rule.max : s.max;
5808
+ const t = hi === lo ? 1 : Math.max(0, Math.min(1, (n - lo) / (hi - lo)));
5809
+ out ?? (out = { style: {} });
5810
+ if (type === "dataBar") {
5811
+ const zero = lo < 0 && hi > 0 ? -lo / (hi - lo) : lo >= 0 ? 0 : 1;
5812
+ const at = hi === lo ? 1 : t;
5813
+ out.bar = { from: Math.min(zero, at), to: Math.max(zero, at), color: rule.color || "var(--fg-accent)" };
5814
+ } else if (type === "colorScale") {
5815
+ const cs = Array.isArray(rule.colors) && rule.colors.length >= 2 ? rule.colors : ["#f8696b", "#ffeb84", "#63be7b"];
5816
+ const bg = cs.length >= 3 ? t < 0.5 ? mixColor(cs[0], cs[1], t * 2) : mixColor(cs[1], cs[2], (t - 0.5) * 2) : mixColor(cs[0], cs[1], t);
5817
+ out.style = { ...out.style, background: bg };
5818
+ } else {
5819
+ const set = ICON_SETS[rule.icons ?? "arrows"] ?? ICON_SETS.arrows;
5820
+ out.icon = t >= 2 / 3 ? set[0] : t >= 1 / 3 ? set[1] : set[2];
5821
+ }
5822
+ continue;
5823
+ }
5824
+ if (this.test(rule, colId, value, row)) {
5825
+ out ?? (out = { style: {} });
5826
+ out.style = { ...out.style, ...rule.style };
5827
+ if (rule.stopIfTrue) break;
5828
+ }
5829
+ }
5830
+ return out;
5831
+ }
5832
+ };
5833
+ function argbOf(c) {
5834
+ const x = c ? hex(c) : null;
5835
+ return x ? `FF${x.map((v) => v.toString(16).padStart(2, "0")).join("").toUpperCase()}` : void 0;
5836
+ }
5837
+
4682
5838
  // src/rows/sort.ts
4683
5839
  function compareNonBlank(a, b, collator) {
4684
5840
  if (typeof a === "number" && typeof b === "number") return a - b;
@@ -4692,7 +5848,7 @@ function normalize(def, v) {
4692
5848
  const n = typeof v === "number" ? v : Number(v);
4693
5849
  return Number.isNaN(n) ? null : n;
4694
5850
  }
4695
- if (def.type === "date") return toDate(v)?.getTime() ?? null;
5851
+ if (def.type === "date") return toDate2(v)?.getTime() ?? null;
4696
5852
  return v;
4697
5853
  }
4698
5854
  function compareValues(a, b, collator) {
@@ -4743,8 +5899,8 @@ function toCsv(rows, sep = ",") {
4743
5899
  return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
4744
5900
  }
4745
5901
  function detectSeparator(text2) {
4746
- const line = text2.slice(0, text2.search(/\r?\n|$/));
4747
- const count = (ch) => line.split(ch).length - 1;
5902
+ const line2 = text2.slice(0, text2.search(/\r?\n|$/));
5903
+ const count = (ch) => line2.split(ch).length - 1;
4748
5904
  const tabs = count(" "), semis = count(";"), commas = count(",");
4749
5905
  if (tabs && tabs >= commas && tabs >= semis) return " ";
4750
5906
  if (semis > commas) return ";";
@@ -4901,12 +6057,12 @@ function colName(i) {
4901
6057
  return s;
4902
6058
  }
4903
6059
  var EPOCH = Date.UTC(1899, 11, 30);
4904
- var DAY2 = 864e5;
6060
+ var DAY3 = 864e5;
4905
6061
  function toSerial(d) {
4906
- return (Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()) - EPOCH) / DAY2;
6062
+ return (Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()) - EPOCH) / DAY3;
4907
6063
  }
4908
6064
  function fromSerial(n) {
4909
- const u = new Date(Math.round(n * DAY2) + EPOCH);
6065
+ const u = new Date(Math.round(n * DAY3) + EPOCH);
4910
6066
  return new Date(u.getUTCFullYear(), u.getUTCMonth(), u.getUTCDate(), u.getUTCHours(), u.getUTCMinutes(), u.getUTCSeconds());
4911
6067
  }
4912
6068
  var BAD_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f￾￿]/g;
@@ -4938,7 +6094,7 @@ function writeXlsx(sheets) {
4938
6094
  ];
4939
6095
  const fillIndex = /* @__PURE__ */ new Map([["FFF2F4F7", 2]]);
4940
6096
  const argb = (c) => c && /^[0-9a-f]{8}$/i.test(c) ? c.toUpperCase() : c && /^[0-9a-f]{6}$/i.test(c) ? `FF${c.toUpperCase()}` : "";
4941
- const fontOf = (bold, color) => {
6097
+ const fontOf2 = (bold, color) => {
4942
6098
  const key = `${bold ? 1 : 0}|${color}`;
4943
6099
  let i = fontIndex.get(key);
4944
6100
  if (i === void 0) {
@@ -4972,7 +6128,7 @@ function writeXlsx(sheets) {
4972
6128
  customFmts.push(fmt);
4973
6129
  fmtIds.set(fmt, id);
4974
6130
  }
4975
- const fontId = fontOf(bold, color);
6131
+ const fontId = fontOf2(bold, color);
4976
6132
  const fillId = fillOf(fill);
4977
6133
  const attrs = `numFmtId="${id}" fontId="${fontId}" fillId="${fillId}" borderId="0" xfId="0"` + (id ? ' applyNumberFormat="1"' : "") + (fontId ? ' applyFont="1"' : "") + (fillId ? ' applyFill="1"' : "") + (align ? ' applyAlignment="1"' : "");
4978
6134
  i = xfs.length;
@@ -5025,9 +6181,13 @@ function writeXlsx(sheets) {
5025
6181
  },
5026
6182
  {
5027
6183
  name: "xl/workbook.xml",
5028
- data: `${XML}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>` + names.map((n, i) => `<sheet name="${esc(n)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets>" + (sheets.some((s) => s.autoFilter && s.freezeRows) ? `<definedNames>${sheets.map((s, i) => {
6184
+ data: `${XML}<workbook xmlns="${NS}" xmlns:r="${REL_NS}"><sheets>` + names.map((n, i) => `<sheet name="${esc(n)}" sheetId="${i + 1}" r:id="rId${i + 1}"/>`).join("") + "</sheets>" + (sheets.some((s) => s.autoFilter && s.freezeRows || s.printTitleRows) ? `<definedNames>${sheets.map((s, i) => {
5029
6185
  const w = Math.max(0, ...s.rows.map((r) => r.length));
5030
- return s.autoFilter && s.freezeRows && w ? `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">'${esc(names[i].replace(/'/g, "''"))}'!$A$${s.freezeRows}:$${colName(w - 1)}$${s.rows.length}</definedName>` : "";
6186
+ const ref = `'${esc(names[i].replace(/'/g, "''"))}'`;
6187
+ const filter = s.autoFilter && s.freezeRows && w ? `<definedName name="_xlnm._FilterDatabase" localSheetId="${i}" hidden="1">${ref}!$A$${s.freezeRows}:$${colName(w - 1)}$${s.rows.length}</definedName>` : "";
6188
+ const n = Math.min(s.printTitleRows ?? 0, s.rows.length);
6189
+ const titles = n > 0 && Number.isInteger(n) ? `<definedName name="_xlnm.Print_Titles" localSheetId="${i}">${ref}!$1:$${n}</definedName>` : "";
6190
+ return filter + titles;
5031
6191
  }).join("")}</definedNames>` : "") + "</workbook>"
5032
6192
  },
5033
6193
  {
@@ -5071,11 +6231,11 @@ async function readXlsx(input) {
5071
6231
  const shared = parseXml(files.get("xl/sharedStrings.xml"));
5072
6232
  const strings = shared ? tags(shared, "si").map((si) => tags(si, "t").map((t) => t.textContent ?? "").join("")) : [];
5073
6233
  const stylesDoc = parseXml(files.get("xl/styles.xml"));
5074
- const custom = new Map(stylesDoc ? tags(stylesDoc, "numFmt").map((f) => [Number(f.getAttribute("numFmtId")), f.getAttribute("formatCode") ?? ""]) : []);
6234
+ const custom2 = new Map(stylesDoc ? tags(stylesDoc, "numFmt").map((f) => [Number(f.getAttribute("numFmtId")), f.getAttribute("formatCode") ?? ""]) : []);
5075
6235
  const cellXfs = stylesDoc ? tags(stylesDoc, "cellXfs")[0] : void 0;
5076
6236
  const dateStyle = (cellXfs ? [...cellXfs.children].filter((x) => x.localName === "xf") : []).map((xf) => {
5077
6237
  const id = Number(xf.getAttribute("numFmtId") ?? 0);
5078
- return BUILTIN_DATE_FMTS.has(id) || custom.has(id) && isDateFormat(custom.get(id));
6238
+ return BUILTIN_DATE_FMTS.has(id) || custom2.has(id) && isDateFormat(custom2.get(id));
5079
6239
  });
5080
6240
  const sheets = [];
5081
6241
  for (const s of tags(workbook, "sheet")) {
@@ -5193,9 +6353,11 @@ tr { break-inside: avoid; page-break-inside: avoid; }
5193
6353
  .num { text-align: right; color: #9ca3af; }
5194
6354
  tr.group td { background: #eef2f7; font-weight: 600; border-top: 1px solid #cbd5e1; break-after: avoid; page-break-after: avoid; }
5195
6355
  tr.group.level-0 td { background: #e2e8f0; }
6356
+ tr.title td { border: 0; padding: 0 0 4px; background: none; }
5196
6357
  tr.total td { font-weight: 700; background: #f3f4f6; border-top: 1.5px solid #111827; border-bottom: 0; }
5197
6358
  `;
5198
- function buildPrintHtml(grid, opts = {}) {
6359
+ var colorOk = (c) => typeof c === "string" && /^(#[0-9a-f]{3,8}|[a-z]+|rgba?\([\d\s.,%]+\)|hsla?\([\d\s.,%deg]+\))$/i.test(c.trim()) ? c.trim() : void 0;
6360
+ function printModel(grid, opts = {}) {
5199
6361
  const cols = exportColumns(grid, opts.columns);
5200
6362
  const loc = grid.store.state.locale;
5201
6363
  const code = loc.code;
@@ -5210,23 +6372,24 @@ function buildPrintHtml(grid, opts = {}) {
5210
6372
  });
5211
6373
  const depth = Math.max(1, ...chains.map((ch) => ch.length));
5212
6374
  const head = [];
5213
- for (let level = 0; level < depth; level++) {
5214
- let tr = level === 0 && numbered ? `<th class="num" rowspan="${depth}">#</th>` : "";
5215
- for (let ci = 0; ci < cols.length; ci++) {
5216
- const chain = chains[ci];
5217
- const leaf = chain.length - 1;
5218
- if (level < leaf) {
5219
- const g = chain[level];
5220
- if (ci > 0 && chains[ci - 1][level] === g) continue;
5221
- let span = 1;
5222
- while (ci + span < cols.length && chains[ci + span][level] === g) span++;
5223
- tr += `<th colspan="${span}"${span > 1 ? ' class="span"' : ""}>${escHtml(header(g.def, g.id))}</th>`;
5224
- } else if (level === leaf) {
5225
- const a = align(cols[ci].def);
5226
- tr += `<th colspan="1" rowspan="${depth - level}"${a === "left" ? "" : ` style="text-align:${a}"`}>${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
5227
- }
5228
- }
5229
- head.push(`<tr>${tr}</tr>`);
6375
+ if (opts.headers !== false) {
6376
+ for (let level = 0; level < depth; level++) {
6377
+ const tr = level === 0 && numbered ? [{ text: "#", align: "right", num: true, rowspan: depth }] : [];
6378
+ for (let ci = 0; ci < cols.length; ci++) {
6379
+ const chain = chains[ci];
6380
+ const leaf = chain.length - 1;
6381
+ if (level < leaf) {
6382
+ const g = chain[level];
6383
+ if (ci > 0 && chains[ci - 1][level] === g) continue;
6384
+ let span = 1;
6385
+ while (ci + span < cols.length && chains[ci + span][level] === g) span++;
6386
+ tr.push({ text: header(g.def, g.id), align: span > 1 ? "center" : "left", span, group: true });
6387
+ } else if (level === leaf) {
6388
+ tr.push({ text: header(cols[ci].def, cols[ci].id), align: align(cols[ci].def), span: 1, rowspan: depth - level });
6389
+ }
6390
+ }
6391
+ head.push(tr);
6392
+ }
5230
6393
  }
5231
6394
  const aggText = (c, i, v, agg) => {
5232
6395
  if (v == null || v === "") return "";
@@ -5234,64 +6397,75 @@ function buildPrintHtml(grid, opts = {}) {
5234
6397
  const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
5235
6398
  return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
5236
6399
  };
5237
- const labelled = (cls, label, values, indent = 0) => {
6400
+ const labelled = (kind, label, values, level = 0) => {
5238
6401
  let lead = 0;
5239
6402
  while (lead < cols.length - 1 && !values[lead]) lead++;
5240
6403
  lead = Math.max(1, lead);
5241
- let tr = numbered ? '<td class="num"></td>' : "";
5242
- tr += `<td colspan="${lead}"${indent ? ` style="padding-left:${7 + indent * 14}px"` : ""}>${escHtml(label)}</td>`;
5243
- for (let i = lead; i < cols.length; i++) tr += `<td style="text-align:${align(cols[i].def)}">${escHtml(values[i] ?? "")}</td>`;
5244
- return `<tr class="${cls}">${tr}</tr>`;
6404
+ const cells = numbered ? [{ text: "", align: "right", num: true }] : [];
6405
+ cells.push({ text: label, align: "left", span: lead, indent: level });
6406
+ for (let i = lead; i < cols.length; i++) cells.push({ text: values[i] ?? "", align: align(cols[i].def) });
6407
+ return { kind, level, cells };
5245
6408
  };
5246
6409
  const rows = exportRows(grid, opts.rows);
5247
6410
  const groupBy = opts.groups !== false && !grid.rowModel.server && !grid.options.treeData ? grid.getGroupBy() : [];
5248
6411
  const probe = opts.styles !== false && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
5249
- let body = "";
6412
+ const body = [];
5250
6413
  try {
5251
- for (const line of printLines(grid, rows, groupBy)) {
5252
- if (line.kind === "group") {
5253
- const def = grid.columns.getNode(line.colId)?.def;
5254
- const value = isBlank(line.value) ? loc.blanks : def ? getFormatter(def, code)(line.value) : String(line.value);
5255
- const label = `${def ? header(def, line.colId) : line.colId}: ${value} (${nf.format(line.rows.length)})`;
6414
+ for (const line2 of printLines(grid, rows, groupBy)) {
6415
+ if (line2.kind === "group") {
6416
+ const def = grid.columns.getNode(line2.colId)?.def;
6417
+ const value = isBlank(line2.value) ? loc.blanks : def ? getFormatter(def, code)(line2.value) : String(line2.value);
6418
+ const label = `${def ? header(def, line2.colId) : line2.colId}: ${value} (${nf.format(line2.rows.length)})`;
5256
6419
  const values = cols.map((c, i) => {
5257
6420
  if (!c.def.aggFunc || groupBy.includes(c.id)) return void 0;
5258
6421
  try {
5259
- return aggText(c, i, aggregate(c.def.aggFunc, line.rows.map((r2) => getCellValue(c.def, r2)), line.rows), c.def.aggFunc) || void 0;
6422
+ return aggText(c, i, aggregate(c.def.aggFunc, line2.rows.map((r2) => getCellValue(c.def, r2)), line2.rows), c.def.aggFunc) || void 0;
5260
6423
  } catch {
5261
6424
  return void 0;
5262
6425
  }
5263
6426
  });
5264
- body += labelled(`group level-${Math.min(line.level, 5)}`, label, values, line.level);
6427
+ body.push(labelled("group", label, values, line2.level));
5265
6428
  continue;
5266
6429
  }
5267
- const r = line.row;
5268
- let tr = numbered ? `<td class="num">${line.n}</td>` : "";
6430
+ const r = line2.row;
6431
+ const cells = numbered ? [{ text: String(line2.n), align: "right", num: true }] : [];
5269
6432
  cols.forEach((c, i) => {
5270
6433
  const v = getCellValue(c.def, r);
5271
6434
  const text2 = isBlank(v) ? "" : fmts[i](v);
5272
- let style = `text-align:${align(c.def)}`;
6435
+ const cell = { text: text2, align: align(c.def) };
5273
6436
  if (probe && c.def.cellClass) {
5274
6437
  let cls;
5275
6438
  try {
5276
- cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: text2, row: r, rowIndex: line.n - 1, column: c.def }) : c.def.cellClass;
6439
+ cls = typeof c.def.cellClass === "function" ? c.def.cellClass({ value: v, formatted: text2, row: r, rowIndex: line2.n - 1, column: c.def }) : c.def.cellClass;
5277
6440
  } catch {
5278
6441
  cls = void 0;
5279
6442
  }
5280
6443
  if (cls) {
5281
6444
  const st = probe.of(cls);
5282
- if (st.color) style += `;color:${hexOf(st.color)}`;
5283
- if (st.fill) style += `;background:${hexOf(st.fill)}`;
5284
- if (st.bold) style += ";font-weight:600";
6445
+ if (st.color) cell.color = hexOf(st.color);
6446
+ if (st.fill) cell.fill = hexOf(st.fill);
6447
+ if (st.bold) cell.bold = true;
5285
6448
  }
5286
6449
  }
5287
- tr += `<td style="${style}">${escHtml(text2)}</td>`;
6450
+ const cf = opts.styles !== false ? grid.cellFormat(r, c.id, v) : null;
6451
+ if (cf) {
6452
+ const s = cf.style;
6453
+ if (colorOk(s.color)) cell.color = colorOk(s.color);
6454
+ if (colorOk(s.background)) cell.fill = colorOk(s.background);
6455
+ if (s.bold) cell.bold = true;
6456
+ if (s.italic) cell.italic = true;
6457
+ if (s.strike) cell.strike = true;
6458
+ if (cf.bar) cell.bar = { from: cf.bar.from, to: cf.bar.to, color: /^#[0-9a-f]{3,8}$/i.test(cf.bar.color) ? cf.bar.color : "#93c5fd" };
6459
+ if (cf.icon) cell.icon = { char: cf.icon.char, color: cf.icon.color };
6460
+ }
6461
+ cells.push(cell);
5288
6462
  });
5289
- body += `<tr>${tr}</tr>`;
6463
+ body.push({ kind: "row", cells });
5290
6464
  }
5291
6465
  } finally {
5292
6466
  probe?.dispose();
5293
6467
  }
5294
- let foot = "";
6468
+ const foot = [];
5295
6469
  const summary = opts.summary !== false ? grid.summary() : null;
5296
6470
  if (summary) {
5297
6471
  summary.opts.rows.forEach((def, ri) => {
@@ -5299,29 +6473,89 @@ function buildPrintHtml(grid, opts = {}) {
5299
6473
  const spec = def.columns?.[c.id];
5300
6474
  return spec === void 0 ? void 0 : aggText(c, i, summary.values[ri]?.[c.id], cellSpec(spec).agg) || void 0;
5301
6475
  });
5302
- foot += labelled("total", def.label ?? "", values);
6476
+ foot.push(labelled("total", def.label ?? "", values));
5303
6477
  });
5304
6478
  }
5305
6479
  const widths = cols.map((c) => Math.max(1, c.width));
5306
6480
  const sum = widths.reduce((a, b) => a + b, 0);
5307
6481
  const paper = PAPERS.includes(opts.paper) ? opts.paper : "A4";
5308
6482
  const orientation = opts.orientation === "portrait" || opts.orientation === "landscape" ? opts.orientation : sum + (numbered ? 40 : 0) > PORTRAIT_WIDTH ? "landscape" : "portrait";
5309
- const fit = opts.fitToWidth !== false;
5310
- const colgroup = fit ? `<colgroup>${numbered ? '<col style="width:40px">' : ""}${widths.map((w) => `<col style="width:${Math.round(w / sum * 1e3) / 10}%">`).join("")}</colgroup>` : "";
5311
- const title = escHtml(opts.title ?? document.title ?? "");
5312
6483
  const printed = opts.printedAt !== false ? new Intl.DateTimeFormat(code, { dateStyle: "medium", timeStyle: "short" }).format(/* @__PURE__ */ new Date()) : "";
5313
- const heading = opts.title || opts.subtitle ? `<div>${opts.title ? `<h1>${title}</h1>` : ""}${opts.subtitle ? `<p class="sub">${escHtml(opts.subtitle)}</p>` : ""}</div>` : "<div></div>";
5314
- const top = opts.title || opts.subtitle || printed ? `<header>${heading}${printed ? `<p class="printed">${escHtml(printed)}</p>` : ""}</header>` : "";
6484
+ const repeatRows = Number.isInteger(opts.repeatRows) && opts.repeatRows > 0 ? Math.min(opts.repeatRows, body.length) : 0;
6485
+ return {
6486
+ lang: code,
6487
+ title: opts.title ?? "",
6488
+ subtitle: opts.subtitle ?? "",
6489
+ printed,
6490
+ footer: opts.footer ?? "",
6491
+ paper,
6492
+ orientation,
6493
+ fit: opts.fitToWidth !== false,
6494
+ numbered,
6495
+ widths: numbered ? [40, ...widths] : widths,
6496
+ head,
6497
+ body,
6498
+ foot,
6499
+ striped: opts.striped !== false,
6500
+ pageNumbers: opts.pageNumbers !== false,
6501
+ repeatHeader: opts.repeatHeader !== false,
6502
+ repeatTitle: !!opts.repeatTitle,
6503
+ repeatRows
6504
+ };
6505
+ }
6506
+ function cellCss(c, head = false) {
6507
+ let s = head ? c.align === "left" || c.group ? "" : `text-align:${c.align}` : `text-align:${c.align}`;
6508
+ const add = (x) => {
6509
+ s += s ? `;${x}` : x;
6510
+ };
6511
+ if (c.indent) add(`padding-left:${7 + c.indent * 14}px`);
6512
+ if (c.color) add(`color:${c.color}`);
6513
+ if (c.fill) add(`background:${c.fill}`);
6514
+ if (c.bold) add("font-weight:600");
6515
+ if (c.italic) add("font-style:italic");
6516
+ if (c.strike) add("text-decoration:line-through");
6517
+ if (c.bar) {
6518
+ const from = (c.bar.from * 100).toFixed(1), to = (c.bar.to * 100).toFixed(1);
6519
+ add(`background-image:linear-gradient(to right,transparent ${from}%,${c.bar.color}66 ${from}%,${c.bar.color}66 ${to}%,transparent ${to}%)`);
6520
+ }
6521
+ return s;
6522
+ }
6523
+ function cellHtml(c, tag) {
6524
+ const attrs = [
6525
+ c.num ? ' class="num"' : c.group && (c.span ?? 1) > 1 ? ' class="span"' : "",
6526
+ c.span !== void 0 && (tag === "th" || c.span > 1) ? ` colspan="${c.span}"` : "",
6527
+ c.rowspan !== void 0 && tag === "th" ? ` rowspan="${c.rowspan}"` : ""
6528
+ ].join("");
6529
+ if (c.num) return `<${tag}${attrs}>${escHtml(c.text)}</${tag}>`;
6530
+ const css = cellCss(c, tag === "th");
6531
+ const icon = c.icon ? `<span style="color:${escHtml(c.icon.color)};margin-right:5px">${escHtml(c.icon.char)}</span>` : "";
6532
+ return `<${tag}${attrs}${css ? ` style="${escHtml(css)}"` : ""}>${icon}${escHtml(c.text)}</${tag}>`;
6533
+ }
6534
+ var lineHtml = (l) => `<tr${l.kind === "group" ? ` class="group level-${Math.min(l.level ?? 0, 5)}"` : l.kind === "total" ? ' class="total"' : ""}>${l.cells.map((c) => cellHtml(c, "td")).join("")}</tr>`;
6535
+ function buildPrintHtml(grid, opts = {}) {
6536
+ const m = printModel(grid, opts);
6537
+ const sum = m.widths.slice(m.numbered ? 1 : 0).reduce((a, b) => a + b, 0);
6538
+ const colgroup = m.fit ? `<colgroup>${m.numbered ? '<col style="width:40px">' : ""}${m.widths.slice(m.numbered ? 1 : 0).map((w) => `<col style="width:${Math.round(w / sum * 1e3) / 10}%">`).join("")}</colgroup>` : "";
6539
+ const title = escHtml(m.title || (typeof document !== "undefined" ? document.title : "") || "");
6540
+ const heading = m.title || m.subtitle ? `<div>${m.title ? `<h1>${title}</h1>` : ""}${m.subtitle ? `<p class="sub">${escHtml(m.subtitle)}</p>` : ""}</div>` : "<div></div>";
6541
+ const top = m.title || m.subtitle || m.printed ? `<header>${heading}${m.printed ? `<p class="printed">${escHtml(m.printed)}</p>` : ""}</header>` : "";
5315
6542
  const small = "font: 8.5px system-ui, sans-serif; color: #6b7280;";
5316
6543
  const page = [
5317
- `size: ${paper} ${orientation}; margin: 12mm 12mm 14mm;`,
5318
- opts.footer ? `@bottom-left { content: ${cssStr(opts.footer)}; ${small} }` : "",
5319
- opts.pageNumbers !== false ? `@bottom-right { content: counter(page) " / " counter(pages); ${small} }` : ""
6544
+ `size: ${m.paper} ${m.orientation}; margin: 12mm 12mm 14mm;`,
6545
+ m.footer ? `@bottom-left { content: ${cssStr(m.footer)}; ${small} }` : "",
6546
+ m.pageNumbers ? `@bottom-right { content: counter(page) " / " counter(pages); ${small} }` : ""
5320
6547
  ].join(" ");
5321
- const stripes = opts.striped !== false ? "tbody tr:not(.group):nth-child(even) td { background: #f9fafb; }" : "";
5322
- return `<!doctype html><html lang="${escHtml(code)}"><head><meta charset="utf-8"><title>${title}</title><style>
6548
+ const stripes = m.striped ? "tbody tr:not(.group):nth-child(even) td { background: #f9fafb; }" : "";
6549
+ const headRows = m.head.map((tr) => `<tr>${tr.map((c) => cellHtml(c, "th")).join("")}</tr>`).join("");
6550
+ const repeated = m.body.slice(0, m.repeatRows).map(lineHtml).join("");
6551
+ const span = Math.max(1, m.widths.length);
6552
+ const titleRow = m.repeatTitle && top ? `<tr class="title"><td colspan="${span}">${top}</td></tr>` : "";
6553
+ const thead = m.repeatHeader ? `${titleRow}${headRows}${repeated}` : `${titleRow}${repeated}`;
6554
+ const body = (m.repeatHeader ? "" : headRows) + m.body.slice(m.repeatRows).map(lineHtml).join("");
6555
+ const foot = m.foot.map(lineHtml).join("");
6556
+ return `<!doctype html><html lang="${escHtml(m.lang)}"><head><meta charset="utf-8"><title>${title}</title><style>
5323
6557
  @page { ${page} }${PRINT_CSS}${stripes}
5324
- </style></head><body>${top}<table${fit ? ' class="fit"' : ""}>${colgroup}<thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody>${foot ? `<tfoot>${foot}</tfoot>` : ""}</table></body></html>`;
6558
+ </style></head><body>${m.repeatTitle ? "" : top}<table${m.fit ? ' class="fit"' : ""}>${colgroup}<thead>${thead}</thead><tbody>${body}</tbody>${foot ? `<tfoot>${foot}</tfoot>` : ""}</table></body></html>`;
5325
6559
  }
5326
6560
  function printHtml(html) {
5327
6561
  if (typeof document === "undefined") return;
@@ -5363,6 +6597,22 @@ function buildCsv(grid, opts = {}) {
5363
6597
  if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
5364
6598
  return toCsv(matrix, sep);
5365
6599
  }
6600
+ function buildJson(grid, opts = {}) {
6601
+ const cols = exportColumns(grid, opts.columns);
6602
+ const code = grid.store.state.locale.code;
6603
+ const fmts = opts.formatted ? cols.map((c) => getFormatter(c.def, code)) : null;
6604
+ const keys = cols.map((c) => opts.keys === "header" ? header(c.def, c.id) : c.id);
6605
+ const rows = exportRows(grid, opts.rows).map((r) => {
6606
+ const o = {};
6607
+ cols.forEach((c, i) => {
6608
+ const v = getCellValue(c.def, r);
6609
+ o[keys[i]] = fmts ? isBlank(v) ? "" : fmts[i](v) : v === void 0 ? null : v;
6610
+ });
6611
+ return o;
6612
+ });
6613
+ const space = typeof opts.space === "number" && opts.space >= 0 ? Math.min(10, opts.space) : 2;
6614
+ return JSON.stringify(rows, null, space);
6615
+ }
5366
6616
  function excelFormat(def) {
5367
6617
  const f = def.format;
5368
6618
  if (def.type === "date") {
@@ -5385,9 +6635,9 @@ function excelFormat(def) {
5385
6635
  return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
5386
6636
  }
5387
6637
  function toArgb(css) {
5388
- const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(css.trim());
5389
- if (hex) {
5390
- const h = hex[1].length === 3 ? [...hex[1]].map((c) => c + c).join("") : hex[1];
6638
+ const hex2 = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(css.trim());
6639
+ if (hex2) {
6640
+ const h = hex2[1].length === 3 ? [...hex2[1]].map((c) => c + c).join("") : hex2[1];
5391
6641
  return `FF${h.toUpperCase()}`;
5392
6642
  }
5393
6643
  const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/i.exec(css.trim());
@@ -5486,7 +6736,7 @@ function buildXlsxSheet(grid, opts = {}) {
5486
6736
  return Number.isFinite(n) ? { v: n, fmt } : String(v);
5487
6737
  }
5488
6738
  case "date": {
5489
- const d = toDate(v);
6739
+ const d = toDate2(v);
5490
6740
  return d ? { v: d, fmt } : String(v);
5491
6741
  }
5492
6742
  case "boolean":
@@ -5511,6 +6761,13 @@ function buildXlsxSheet(grid, opts = {}) {
5511
6761
  }
5512
6762
  if (cls) style = { ...style, ...probe.of(cls) };
5513
6763
  }
6764
+ const cf = grid.cellFormat(r, c.id, v);
6765
+ if (cf) {
6766
+ const color = argbOf(cf.style.color), fill = argbOf(cf.style.background);
6767
+ if (color) style.color = color;
6768
+ if (fill) style.fill = fill;
6769
+ if (cf.style.bold) style.bold = true;
6770
+ }
5514
6771
  if (!Object.keys(style).length) return cell;
5515
6772
  return cell !== null && typeof cell === "object" && !(cell instanceof Date) ? { ...cell, ...style } : { v: cell, ...style };
5516
6773
  }));
@@ -5544,6 +6801,8 @@ function buildXlsxSheet(grid, opts = {}) {
5544
6801
  widths: cols.map((c) => Math.round(c.width / 7)),
5545
6802
  merges,
5546
6803
  freezeRows: headRows,
6804
+ // header rows (and repeatRows data rows) print on every page
6805
+ printTitleRows: opts.repeatHeader === false ? 0 : headRows + (Number.isInteger(opts.repeatRows) && opts.repeatRows > 0 ? opts.repeatRows : 0),
5547
6806
  autoFilter: headRows > 0
5548
6807
  };
5549
6808
  }
@@ -5612,13 +6871,13 @@ async function importInto(grid, file, opts = {}) {
5612
6871
  const sample = grid.store.state.data[0];
5613
6872
  let skipped = 0;
5614
6873
  const rows = [];
5615
- for (const line of matrix.slice(headRows)) {
5616
- if (!line || line.every((v) => v === null || v === void 0 || String(v).trim() === "")) continue;
6874
+ for (const line2 of matrix.slice(headRows)) {
6875
+ if (!line2 || line2.every((v) => v === null || v === void 0 || String(v).trim() === "")) continue;
5617
6876
  const row = {};
5618
6877
  mapped.forEach((node, i) => {
5619
6878
  if (!node) return;
5620
6879
  const def = node.def;
5621
- const raw = asText(line[i]);
6880
+ const raw = asText(line2[i]);
5622
6881
  const res = grid.acceptText(def, row, raw, sample?.[def.field]);
5623
6882
  if (res.ok) {
5624
6883
  if (!(raw === "" && (res.value === null || res.value === ""))) row[def.field] = res.value;
@@ -6094,7 +7353,7 @@ function buildPivot(source, config, defOf, labels, locale) {
6094
7353
  }
6095
7354
 
6096
7355
  // src/version.ts
6097
- var version = "1.1.0";
7356
+ var version = "1.6.0";
6098
7357
 
6099
7358
  // src/license.ts
6100
7359
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -6282,6 +7541,7 @@ var ColumnMenu = class {
6282
7541
  ];
6283
7542
  if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
6284
7543
  items.push([loc.chooseColumns, () => this.grid.chooser.open()], [loc.resetColumns, () => this.grid.resetColumns()]);
7544
+ if (this.grid.options.conditionalFormatMenu !== false) items.push([`${loc.cf.title}\u2026`, () => this.grid.formatDialog.open(colId)]);
6285
7545
  popup.append(this.section(items));
6286
7546
  const filter = this.filterSection(colId, () => this.close());
6287
7547
  if (filter) popup.append(filter);
@@ -6328,11 +7588,11 @@ var ColumnMenu = class {
6328
7588
  }
6329
7589
  section(items) {
6330
7590
  const s = el("div", "fg-menu-section");
6331
- for (const [label, run] of items) {
7591
+ for (const [label, run2] of items) {
6332
7592
  const b = el("button", "fg-menu-item", { type: "button" });
6333
7593
  b.textContent = label;
6334
7594
  b.addEventListener("click", () => {
6335
- run();
7595
+ run2();
6336
7596
  this.close();
6337
7597
  });
6338
7598
  s.append(b);
@@ -7464,6 +8724,611 @@ var FilterBuilder = class {
7464
8724
  }
7465
8725
  };
7466
8726
 
8727
+ // src/interactions/formatDialog.ts
8728
+ var CF_PRESETS = [
8729
+ { key: "red", style: { background: "#ffc7ce", color: "#9c0006" } },
8730
+ { key: "yellow", style: { background: "#ffeb9c", color: "#9c5700" } },
8731
+ { key: "green", style: { background: "#c6efce", color: "#006100" } },
8732
+ { key: "blue", style: { background: "#dbeafe", color: "#1e3a8a" } },
8733
+ { key: "redText", style: { color: "#dc2626" } },
8734
+ { key: "bold", style: { bold: true } }
8735
+ ];
8736
+ var TYPES2 = ["value", "formula", "top", "bottom", "aboveAverage", "belowAverage", "duplicate", "unique", "dataBar", "colorScale", "iconSet"];
8737
+ var OPS2 = ["gt", "gte", "lt", "lte", "eq", "ne", "between", "notBetween", "contains", "notContains", "startsWith", "endsWith", "blank", "notBlank"];
8738
+ var NO_VALUE3 = ["blank", "notBlank"];
8739
+ var SCALES = ["dataBar", "colorScale", "iconSet"];
8740
+ var FormatDialog = class {
8741
+ constructor(grid) {
8742
+ this.grid = grid;
8743
+ this.popup = null;
8744
+ this.off = [];
8745
+ }
8746
+ get isOpen() {
8747
+ return !!this.popup;
8748
+ }
8749
+ open(colId) {
8750
+ this.close();
8751
+ const { grid } = this;
8752
+ const node = grid.columns.getNode(colId);
8753
+ if (!node || node.children || isSystemCol(colId)) return;
8754
+ const loc = grid.store.state.locale;
8755
+ const cf = loc.cf;
8756
+ const label = node.def.header ?? node.def.field ?? colId;
8757
+ const popup = el("div", `fg-menu fg-cf-dialog fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": `${cf.title}: ${label}` });
8758
+ grid.applyFontTo(popup);
8759
+ const head = el("div", "fg-chooser-head");
8760
+ const title = el("span", "fg-menu-title");
8761
+ title.textContent = `${cf.title} \xB7 ${label}`;
8762
+ const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
8763
+ x.textContent = "\xD7";
8764
+ x.addEventListener("click", () => this.close());
8765
+ head.append(title, x);
8766
+ const list = el("div", "fg-cf-list");
8767
+ const opName = (op) => cf.ops[op] ?? loc.ops[op] ?? op;
8768
+ const describe = (r2) => {
8769
+ const type = r2.type ?? "value";
8770
+ const name = cf.types[type];
8771
+ if (type === "value") {
8772
+ const op = r2.op ?? "eq";
8773
+ if (NO_VALUE3.includes(op)) return opName(op);
8774
+ return `${opName(op)} ${String(r2.value ?? "")}${op === "between" || op === "notBetween" ? ` \u2013 ${String(r2.valueTo ?? "")}` : ""}`;
8775
+ }
8776
+ if (type === "formula") return `${name}: ${r2.formula ?? ""}`;
8777
+ if (type === "top" || type === "bottom") return `${name} ${r2.rank ?? 10}${r2.percent ? "%" : ""}`;
8778
+ return name;
8779
+ };
8780
+ const paintList = () => {
8781
+ list.replaceChildren();
8782
+ const all2 = grid.getConditionalFormats();
8783
+ const mine = all2.map((r2, i) => ({ r: r2, i })).filter(({ r: r2 }) => r2.columns.includes(colId));
8784
+ if (!mine.length) {
8785
+ const empty = el("div", "fg-cf-empty");
8786
+ empty.textContent = cf.none;
8787
+ list.append(empty);
8788
+ return;
8789
+ }
8790
+ for (const { r: r2, i } of mine) {
8791
+ const row = el("div", "fg-cf-rule");
8792
+ const sample = el("span", "fg-cf-sample", { "aria-hidden": "true" });
8793
+ sample.textContent = r2.type === "iconSet" ? "\u25B2" : r2.type === "dataBar" ? "\u25AC" : "Aa";
8794
+ if (r2.style?.background) sample.style.background = r2.style.background;
8795
+ if (r2.style?.color) sample.style.color = r2.style.color;
8796
+ if (r2.style?.bold) sample.style.fontWeight = "700";
8797
+ if (r2.type === "colorScale") sample.style.background = `linear-gradient(to right, ${(r2.colors ?? ["#f8696b", "#ffeb84", "#63be7b"]).join(",")})`;
8798
+ const text2 = el("span", "fg-cf-rule-text");
8799
+ text2.textContent = `${describe(r2)}${r2.row ? ` \xB7 ${cf.wholeRow}` : ""}`;
8800
+ text2.title = text2.textContent;
8801
+ const del = el("button", "fg-chooser-close", { type: "button", "aria-label": `${loc.close}: ${text2.textContent}` });
8802
+ del.textContent = "\xD7";
8803
+ del.addEventListener("click", () => {
8804
+ const next = grid.getConditionalFormats();
8805
+ const rule = next[i];
8806
+ rule.columns = rule.columns.filter((c) => c !== colId);
8807
+ grid.setConditionalFormats(next.filter((f) => f.columns.length));
8808
+ paintList();
8809
+ });
8810
+ row.append(sample, text2, del);
8811
+ list.append(row);
8812
+ }
8813
+ };
8814
+ paintList();
8815
+ const form = el("div", "fg-cf-form");
8816
+ const typeSel = el("select", "fg-filter-op fg-cf-wide fg-cf-type", { "aria-label": cf.type });
8817
+ for (const t of TYPES2) {
8818
+ const o = el("option");
8819
+ o.value = t;
8820
+ o.textContent = cf.types[t];
8821
+ typeSel.append(o);
8822
+ }
8823
+ const opSel = el("select", "fg-filter-op fg-cf-wide fg-cf-op", { "aria-label": loc.filter });
8824
+ for (const op of OPS2) {
8825
+ const o = el("option");
8826
+ o.value = op;
8827
+ o.textContent = opName(op);
8828
+ opSel.append(o);
8829
+ }
8830
+ const numeric = node.def.type === "number";
8831
+ const v1 = el("input", "fg-input fg-cf-value", { type: numeric ? "number" : "text", placeholder: loc.value, "aria-label": loc.value });
8832
+ const v2 = el("input", "fg-input fg-cf-value-to", { type: numeric ? "number" : "text", placeholder: loc.valueTo, "aria-label": loc.valueTo });
8833
+ const formula = el("input", "fg-input fg-cf-wide fg-cf-formula", { type: "text", placeholder: `=${node.def.field ?? colId} > 100`, "aria-label": cf.types.formula, spellcheck: "false" });
8834
+ const rank = el("input", "fg-input fg-cf-rank", { type: "number", min: "1", value: "10", "aria-label": cf.count });
8835
+ const pctLabel = el("label", "fg-cf-pct");
8836
+ const pct = el("input", "", { type: "checkbox" });
8837
+ pctLabel.append(pct, " %");
8838
+ const swatches = el("div", "fg-cf-swatches fg-cf-wide", { role: "group", "aria-label": cf.style });
8839
+ let preset = 0;
8840
+ CF_PRESETS.forEach((p, k) => {
8841
+ const b = el("button", "fg-cf-swatch", { type: "button", "aria-label": cf.styles[p.key], title: cf.styles[p.key], "aria-pressed": String(k === preset) });
8842
+ b.textContent = p.key === "bold" ? "B" : "Aa";
8843
+ if (p.style.background) b.style.background = p.style.background;
8844
+ if (p.style.color) b.style.color = p.style.color;
8845
+ b.addEventListener("click", () => {
8846
+ preset = k;
8847
+ for (const s of swatches.children) s.setAttribute("aria-pressed", String(s === b));
8848
+ });
8849
+ swatches.append(b);
8850
+ });
8851
+ const rowLabel = el("label", "fg-cf-wide fg-cf-row");
8852
+ const rowBox = el("input", "", { type: "checkbox" });
8853
+ rowLabel.append(rowBox, ` ${cf.wholeRow}`);
8854
+ form.append(typeSel, opSel, v1, v2, formula, rank, pctLabel, swatches, rowLabel);
8855
+ const sync = () => {
8856
+ const t = typeSel.value;
8857
+ const op = opSel.value;
8858
+ opSel.hidden = t !== "value";
8859
+ v1.hidden = t !== "value" || NO_VALUE3.includes(op);
8860
+ v2.hidden = t !== "value" || op !== "between" && op !== "notBetween";
8861
+ formula.hidden = t !== "formula";
8862
+ rank.hidden = pctLabel.hidden = t !== "top" && t !== "bottom";
8863
+ swatches.hidden = SCALES.includes(t);
8864
+ rowLabel.hidden = t !== "value" && t !== "formula";
8865
+ };
8866
+ typeSel.addEventListener("change", sync);
8867
+ opSel.addEventListener("change", sync);
8868
+ sync();
8869
+ const read = () => {
8870
+ const type = typeSel.value;
8871
+ const rule = { columns: [colId], type };
8872
+ const val = (input) => numeric && input.value !== "" ? Number(input.value) : input.value;
8873
+ if (type === "value") {
8874
+ rule.op = opSel.value;
8875
+ if (!NO_VALUE3.includes(rule.op)) {
8876
+ if (v1.value === "") return null;
8877
+ rule.value = val(v1);
8878
+ if (rule.op === "between" || rule.op === "notBetween") {
8879
+ if (v2.value === "") return null;
8880
+ rule.valueTo = val(v2);
8881
+ }
8882
+ }
8883
+ } else if (type === "formula") {
8884
+ const f = formula.value.trim();
8885
+ if (!f) return null;
8886
+ rule.formula = f.startsWith("=") ? f : `=${f}`;
8887
+ } else if (type === "top" || type === "bottom") {
8888
+ rule.rank = Math.max(1, Math.floor(Number(rank.value) || 10));
8889
+ if (pct.checked) rule.percent = true;
8890
+ }
8891
+ if (!SCALES.includes(type)) rule.style = { ...CF_PRESETS[preset].style };
8892
+ if (rowBox.checked && (type === "value" || type === "formula")) rule.row = true;
8893
+ return rule;
8894
+ };
8895
+ const actions = el("div", "fg-menu-actions");
8896
+ const clear = el("button", "fg-btn", { type: "button" });
8897
+ clear.textContent = cf.clear;
8898
+ clear.addEventListener("click", () => {
8899
+ grid.setConditionalFormats(grid.getConditionalFormats().map((r2) => ({ ...r2, columns: r2.columns.filter((c) => c !== colId) })).filter((r2) => r2.columns.length));
8900
+ paintList();
8901
+ });
8902
+ const add = el("button", "fg-btn fg-btn-primary", { type: "button" });
8903
+ add.textContent = cf.add;
8904
+ add.addEventListener("click", () => {
8905
+ const rule = read();
8906
+ if (!rule) {
8907
+ (typeSel.value === "formula" ? formula : v1).focus();
8908
+ return;
8909
+ }
8910
+ grid.setConditionalFormats([...grid.getConditionalFormats(), rule]);
8911
+ v1.value = v2.value = formula.value = "";
8912
+ paintList();
8913
+ });
8914
+ actions.append(clear, add);
8915
+ popup.append(head, list, form, actions);
8916
+ popupParent(grid.renderer.root).append(popup);
8917
+ const r = grid.renderer.root.getBoundingClientRect();
8918
+ const w = popup.offsetWidth || 340;
8919
+ popup.style.left = `${Math.max(4, Math.min(r.left + 24, (window.innerWidth || 1024) - w - 4))}px`;
8920
+ popup.style.top = `${Math.max(4, r.top + 24)}px`;
8921
+ this.popup = popup;
8922
+ typeSel.focus();
8923
+ const onDown = (e) => {
8924
+ if (!eventInside(e, popup)) this.close();
8925
+ };
8926
+ const onKey = (e) => {
8927
+ if (e.key === "Escape") this.close();
8928
+ };
8929
+ document.addEventListener("pointerdown", onDown, true);
8930
+ document.addEventListener("keydown", onKey, true);
8931
+ this.off.push(
8932
+ () => document.removeEventListener("pointerdown", onDown, true),
8933
+ () => document.removeEventListener("keydown", onKey, true)
8934
+ );
8935
+ }
8936
+ close() {
8937
+ for (const f of this.off) f();
8938
+ this.off = [];
8939
+ this.popup?.remove();
8940
+ this.popup = null;
8941
+ }
8942
+ };
8943
+
8944
+ // src/assistant.ts
8945
+ var FILTER_TYPES = ["text", "number", "date", "set", "combined"];
8946
+ var CHART_TYPES = ["bar", "stackedBar", "line", "area", "pie", "donut", "combo", "scatter"];
8947
+ var MAX_ACTIONS = 30;
8948
+ function describeColumn(grid, id, def, rows) {
8949
+ const col = { id, header: def.header ?? def.field ?? id };
8950
+ if (def.type) col.type = def.type;
8951
+ if (def.formula) col.formula = def.formula;
8952
+ if (def.aggFunc && typeof def.aggFunc === "string") col.aggFunc = def.aggFunc;
8953
+ let blanks = 0;
8954
+ const counts = /* @__PURE__ */ new Map();
8955
+ let min = Infinity, max = -Infinity, sum = 0, nums = 0;
8956
+ let minText = "", maxText = "";
8957
+ for (const r of rows) {
8958
+ const v = getCellValue(def, r);
8959
+ if (isBlank(v)) {
8960
+ blanks++;
8961
+ continue;
8962
+ }
8963
+ if (typeof v === "number" && Number.isFinite(v)) {
8964
+ nums++;
8965
+ sum += v;
8966
+ if (v < min) min = v;
8967
+ if (v > max) max = v;
8968
+ } else if (def.type === "date") {
8969
+ const t = v instanceof Date ? v.toISOString().slice(0, 10) : String(v);
8970
+ if (!minText || t < minText) minText = t;
8971
+ if (!maxText || t > maxText) maxText = t;
8972
+ }
8973
+ if (counts.size < 5e3) {
8974
+ const k = setKey(v);
8975
+ const c = counts.get(k);
8976
+ if (c) c.n++;
8977
+ else counts.set(k, { value: v, n: 1 });
8978
+ }
8979
+ }
8980
+ col.blanks = blanks;
8981
+ col.distinct = counts.size;
8982
+ if (nums) Object.assign(col, { min, max, avg: Math.round(sum / nums * 1e3) / 1e3 });
8983
+ if (minText) Object.assign(col, { min: minText, max: maxText });
8984
+ if (!nums && counts.size) {
8985
+ col.top = [...counts.values()].sort((a, b) => b.n - a.n).slice(0, 12).map((c) => ({ value: String(c.value).slice(0, 80), count: c.n }));
8986
+ }
8987
+ return col;
8988
+ }
8989
+ function assistantRequest(grid, prompt, options) {
8990
+ const rows = grid.rowModel.dataRows().map((n) => n.data);
8991
+ const all2 = grid.columns.getState().map((s2) => grid.columns.getNode(s2.colId)).filter((n) => n && !n.children && !isSystemCol(n.id));
8992
+ const columns = all2.map((n) => ({ ...describeColumn(grid, n.id, n.def, rows), hidden: grid.columns.getState().find((s2) => s2.colId === n.id)?.hide || void 0 }));
8993
+ const sampleN = typeof options.sampleRows === "number" && options.sampleRows >= 0 ? Math.min(50, options.sampleRows) : 5;
8994
+ const sample = rows.slice(0, sampleN).map((r) => Object.fromEntries(all2.map((n) => [n.id, getCellValue(n.def, r)])));
8995
+ const s = grid.store.state;
8996
+ return {
8997
+ prompt: String(prompt).slice(0, 2e3),
8998
+ locale: s.locale.code,
8999
+ rowCount: rows.length,
9000
+ totalRows: grid.rowModel.server ? rows.length : grid.store.state.data.length,
9001
+ columns,
9002
+ sample,
9003
+ state: {
9004
+ sort: s.sort,
9005
+ filters: s.filters,
9006
+ quickFilter: s.quickFilter,
9007
+ advancedFilter: s.advancedFilter,
9008
+ groupBy: grid.getGroupBy(),
9009
+ pivot: grid.getPivot(),
9010
+ conditionalFormats: grid.getConditionalFormats()
9011
+ },
9012
+ ...options.context ? { context: options.context() } : {}
9013
+ };
9014
+ }
9015
+ function applyAssistantActions(grid, actions) {
9016
+ const before = { state: grid.api.getState(), defs: grid.columnDefs(), summary: grid.summaryOptions() };
9017
+ const applied = [];
9018
+ const skipped = [];
9019
+ const charts = [];
9020
+ const list = Array.isArray(actions) ? actions.slice(0, MAX_ACTIONS) : [];
9021
+ const known = (id) => typeof id === "string" && !!grid.columns.getNode(id) && !grid.columns.getNode(id).children && !isSystemCol(id);
9022
+ const ok = (a) => applied.push(a);
9023
+ const skip = (a, reason) => skipped.push({ action: a, reason });
9024
+ for (const raw of list) {
9025
+ const a = raw;
9026
+ if (!a || typeof a !== "object" || typeof a.type !== "string") {
9027
+ skip(raw, "not an action");
9028
+ continue;
9029
+ }
9030
+ try {
9031
+ switch (a.type) {
9032
+ case "sort": {
9033
+ const sort = (Array.isArray(a.sort) ? a.sort : []).filter((x) => !!x && known(x.colId) && (x.dir === "asc" || x.dir === "desc"));
9034
+ if (Array.isArray(a.sort) && sort.length !== a.sort.length) skip(a, "unknown column in sort");
9035
+ grid.setSort(sort.map((x) => ({ colId: x.colId, dir: x.dir })));
9036
+ ok(a);
9037
+ break;
9038
+ }
9039
+ case "filter": {
9040
+ if (!known(a.colId)) {
9041
+ skip(a, "unknown column");
9042
+ break;
9043
+ }
9044
+ const m = a.model;
9045
+ if (m !== null && (!m || typeof m !== "object" || !FILTER_TYPES.includes(m.type))) {
9046
+ skip(a, "bad filter model");
9047
+ break;
9048
+ }
9049
+ grid.setFilter(a.colId, m);
9050
+ ok(a);
9051
+ break;
9052
+ }
9053
+ case "clearFilters":
9054
+ for (const id of Object.keys(grid.store.state.filters)) grid.setFilter(id, null);
9055
+ grid.setAdvancedFilter(null);
9056
+ grid.api.setQuickFilter("");
9057
+ ok(a);
9058
+ break;
9059
+ case "quickFilter":
9060
+ grid.api.setQuickFilter(typeof a.text === "string" ? a.text.slice(0, 200) : "");
9061
+ ok(a);
9062
+ break;
9063
+ case "advancedFilter": {
9064
+ const f = a.filter;
9065
+ const ids = [];
9066
+ const walk = (x) => {
9067
+ const o = x;
9068
+ if (Array.isArray(o?.conditions)) o.conditions.forEach(walk);
9069
+ else ids.push(o?.colId);
9070
+ };
9071
+ if (f) walk(f);
9072
+ if (f && (!Array.isArray(f.conditions) || !ids.every(known))) {
9073
+ skip(a, "unknown column in advanced filter");
9074
+ break;
9075
+ }
9076
+ grid.setAdvancedFilter(f ?? null);
9077
+ ok(a);
9078
+ break;
9079
+ }
9080
+ case "groupBy": {
9081
+ const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9082
+ grid.setGroupBy(ids);
9083
+ if (a.expand) grid.expandAll(true);
9084
+ ok(a);
9085
+ break;
9086
+ }
9087
+ case "pivot": {
9088
+ const p = a.pivot;
9089
+ if (p && (!Array.isArray(p.rows) || !Array.isArray(p.values) || !p.rows.every(known) || !(p.columns ?? []).every(known) || !p.values.every((v) => v && known(v.colId)))) {
9090
+ skip(a, "unknown column in pivot");
9091
+ break;
9092
+ }
9093
+ grid.setPivot(p ?? null);
9094
+ ok(a);
9095
+ break;
9096
+ }
9097
+ case "showColumns":
9098
+ case "hideColumns": {
9099
+ const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9100
+ if (ids.length) grid.setColumnsVisible(ids, a.type === "showColumns");
9101
+ ok(a);
9102
+ break;
9103
+ }
9104
+ case "conditionalFormat": {
9105
+ const f = a.format;
9106
+ if (!f || !Array.isArray(f.columns) || !f.columns.length || !f.columns.every(known)) {
9107
+ skip(a, "unknown column in format");
9108
+ break;
9109
+ }
9110
+ grid.setConditionalFormats([...grid.getConditionalFormats(), f]);
9111
+ ok(a);
9112
+ break;
9113
+ }
9114
+ case "clearConditionalFormats":
9115
+ grid.setConditionalFormats([]);
9116
+ ok(a);
9117
+ break;
9118
+ case "addColumn": {
9119
+ const c = a.column;
9120
+ if (!c || typeof c.id !== "string" || !/^[\w.-]{1,40}$/.test(c.id) || grid.columns.getNode(c.id)) {
9121
+ skip(a, "bad or taken column id");
9122
+ break;
9123
+ }
9124
+ const f = typeof c.formula === "string" ? c.formula.startsWith("=") ? c.formula : `=${c.formula}` : "";
9125
+ const refs = f ? formulaRefs(f) : null;
9126
+ if (!refs) {
9127
+ skip(a, "formula does not parse");
9128
+ break;
9129
+ }
9130
+ const def = { id: c.id, header: typeof c.header === "string" ? c.header.slice(0, 80) : c.id, formula: f };
9131
+ if (c.type === "number" || c.type === "date" || c.type === "text") def.type = c.type;
9132
+ grid.api.setColumns([...grid.columnDefs(), def]);
9133
+ ok(a);
9134
+ break;
9135
+ }
9136
+ case "summary": {
9137
+ const s = a.summary;
9138
+ if (s && (!Array.isArray(s.rows) || !s.rows.every((r) => r && Object.keys(r.columns ?? {}).every(known)))) {
9139
+ skip(a, "unknown column in summary");
9140
+ break;
9141
+ }
9142
+ grid.setSummary(s ?? null);
9143
+ ok(a);
9144
+ break;
9145
+ }
9146
+ case "chart": {
9147
+ const ids = Array.isArray(a.colIds) ? a.colIds.filter(known) : [];
9148
+ const displayed = grid.columns.displayed().map((c) => c.id);
9149
+ const idx = ids.map((id) => displayed.indexOf(id)).filter((i) => i >= 0);
9150
+ const count = grid.rowModel.getRowCount();
9151
+ if (!idx.length || !count) {
9152
+ skip(a, "nothing to chart");
9153
+ break;
9154
+ }
9155
+ const range = { start: { rowIndex: 0, colId: displayed[Math.min(...idx)] }, end: { rowIndex: Math.min(count, 200) - 1, colId: displayed[Math.max(...idx)] } };
9156
+ charts.push(grid.api.createChart({ type: CHART_TYPES.includes(a.chartType) ? a.chartType : "bar", range, title: typeof a.title === "string" ? a.title.slice(0, 80) : void 0 }));
9157
+ ok(a);
9158
+ break;
9159
+ }
9160
+ case "message":
9161
+ ok(a);
9162
+ break;
9163
+ default:
9164
+ skip(a, "unknown action");
9165
+ }
9166
+ } catch {
9167
+ skip(a, "failed");
9168
+ }
9169
+ }
9170
+ const undo = () => {
9171
+ for (const c of charts) c.destroy();
9172
+ if (grid.columnDefs().length !== before.defs.length || grid.columnDefs().some((d, i) => d !== before.defs[i])) grid.api.setColumns(before.defs);
9173
+ grid.setSummary(before.summary);
9174
+ grid.api.setState(before.state);
9175
+ if (!before.state.conditionalFormats) grid.setConditionalFormats([]);
9176
+ };
9177
+ return { applied, skipped, undo };
9178
+ }
9179
+ async function askAssistant(grid, prompt, options, signal) {
9180
+ if (!options?.url) throw new Error("assistant.url is not set");
9181
+ const headers = typeof options.headers === "function" ? options.headers() : options.headers ?? {};
9182
+ const res = await fetch(options.url, {
9183
+ method: "POST",
9184
+ credentials: options.credentials ?? "same-origin",
9185
+ headers: { "Content-Type": "application/json", Accept: "application/json", ...headers },
9186
+ body: JSON.stringify(assistantRequest(grid, prompt, options)),
9187
+ signal
9188
+ });
9189
+ if (!res.ok) throw new Error(`HTTP ${res.status} from the assistant`);
9190
+ const json = await res.json();
9191
+ const result = applyAssistantActions(grid, Array.isArray(json?.actions) ? json.actions : []);
9192
+ const say = typeof json?.message === "string" ? json.message : result.applied.filter((a) => a.type === "message").map((a) => a.text ?? "").join("\n");
9193
+ return { ...result, message: say.slice(0, 4e3) };
9194
+ }
9195
+
9196
+ // src/interactions/assistantPanel.ts
9197
+ var AssistantPanel = class {
9198
+ constructor(grid) {
9199
+ this.grid = grid;
9200
+ this.popup = null;
9201
+ this.log = null;
9202
+ this.off = [];
9203
+ this.busy = null;
9204
+ }
9205
+ get isOpen() {
9206
+ return !!this.popup;
9207
+ }
9208
+ open() {
9209
+ if (this.popup) {
9210
+ this.popup.querySelector(".fg-ai-input")?.focus();
9211
+ return;
9212
+ }
9213
+ const { grid } = this;
9214
+ const loc = grid.store.state.locale;
9215
+ const opts = grid.options.assistant;
9216
+ const popup = el("div", `fg-menu fg-ai-panel fg-theme-${grid.store.state.theme}`, { role: "dialog", "aria-label": loc.ai.title });
9217
+ grid.applyFontTo(popup);
9218
+ const head = el("div", "fg-chooser-head");
9219
+ const title = el("span", "fg-menu-title");
9220
+ title.textContent = loc.ai.title;
9221
+ const x = el("button", "fg-chooser-close", { type: "button", "aria-label": loc.close });
9222
+ x.textContent = "\xD7";
9223
+ x.addEventListener("click", () => this.close());
9224
+ head.append(title, x);
9225
+ const log = el("div", "fg-ai-log", { role: "log", "aria-live": "polite" });
9226
+ this.log = log;
9227
+ const form = el("form", "fg-ai-form");
9228
+ const input = el("textarea", "fg-input fg-ai-input", { rows: "2", placeholder: loc.ai.placeholder, "aria-label": loc.ai.placeholder, maxlength: "2000" });
9229
+ const send2 = el("button", "fg-btn fg-btn-primary fg-ai-send", { type: "submit" });
9230
+ send2.textContent = loc.ai.send;
9231
+ form.append(input, send2);
9232
+ const tips = el("div", "fg-ai-tips");
9233
+ for (const s of (opts?.suggestions ?? []).slice(0, 6)) {
9234
+ const b = el("button", "fg-ai-tip", { type: "button" });
9235
+ b.textContent = s;
9236
+ b.addEventListener("click", () => {
9237
+ input.value = s;
9238
+ input.focus();
9239
+ });
9240
+ tips.append(b);
9241
+ }
9242
+ input.addEventListener("keydown", (e) => {
9243
+ if (e.key === "Enter" && !e.shiftKey && !e.isComposing) {
9244
+ e.preventDefault();
9245
+ form.requestSubmit?.() ?? form.dispatchEvent(new Event("submit", { cancelable: true }));
9246
+ }
9247
+ });
9248
+ form.addEventListener("submit", (e) => {
9249
+ e.preventDefault();
9250
+ const prompt = input.value.trim();
9251
+ if (!prompt || this.busy) return;
9252
+ input.value = "";
9253
+ void this.ask(prompt, send2);
9254
+ });
9255
+ popup.append(head, log, form);
9256
+ if (tips.childElementCount) popup.append(tips);
9257
+ popupParent(grid.renderer.root).append(popup);
9258
+ const r = grid.renderer.root.getBoundingClientRect();
9259
+ const w = popup.offsetWidth || 360;
9260
+ popup.style.left = `${Math.max(4, Math.min(r.right - w - 16, (window.innerWidth || 1024) - w - 4))}px`;
9261
+ popup.style.top = `${Math.max(4, r.top + 16)}px`;
9262
+ this.popup = popup;
9263
+ input.focus();
9264
+ const onDown = (e) => {
9265
+ if (!eventInside(e, popup) && !this.busy) this.close();
9266
+ };
9267
+ const onKey = (e) => {
9268
+ if (e.key === "Escape") this.close();
9269
+ };
9270
+ document.addEventListener("pointerdown", onDown, true);
9271
+ document.addEventListener("keydown", onKey, true);
9272
+ this.off.push(
9273
+ () => document.removeEventListener("pointerdown", onDown, true),
9274
+ () => document.removeEventListener("keydown", onKey, true)
9275
+ );
9276
+ }
9277
+ say(who, text2) {
9278
+ const msg = el("div", `fg-ai-msg fg-ai-${who}`);
9279
+ msg.textContent = text2;
9280
+ this.log?.append(msg);
9281
+ if (this.log) this.log.scrollTop = this.log.scrollHeight;
9282
+ return msg;
9283
+ }
9284
+ async ask(prompt, send2) {
9285
+ const loc = this.grid.store.state.locale;
9286
+ this.say("user", prompt);
9287
+ const wait = this.say("ai", loc.ai.thinking);
9288
+ wait.classList.add("fg-ai-wait");
9289
+ this.busy = new AbortController();
9290
+ send2.disabled = true;
9291
+ try {
9292
+ const res = await askAssistant(this.grid, prompt, this.grid.options.assistant, this.busy.signal);
9293
+ wait.classList.remove("fg-ai-wait");
9294
+ const changed = res.applied.filter((a) => a.type !== "message").length;
9295
+ wait.textContent = res.message || (changed ? loc.ai.done : "");
9296
+ if (res.skipped.length) {
9297
+ const note = el("div", "fg-ai-note");
9298
+ note.textContent = `${loc.ai.skipped}: ${res.skipped.map((s) => s.reason).join(", ")}`;
9299
+ wait.append(note);
9300
+ }
9301
+ if (changed) {
9302
+ const undo = el("button", "fg-btn fg-ai-undo", { type: "button" });
9303
+ undo.textContent = loc.ai.undo;
9304
+ undo.addEventListener("click", () => {
9305
+ res.undo();
9306
+ undo.disabled = true;
9307
+ undo.textContent = loc.ai.undone;
9308
+ });
9309
+ wait.append(undo);
9310
+ }
9311
+ this.grid.events.emit("assistantAnswer", { prompt, message: res.message, applied: res.applied, skipped: res.skipped });
9312
+ } catch (e) {
9313
+ if (e?.name === "AbortError") return;
9314
+ wait.remove();
9315
+ this.say("error", loc.ai.failed);
9316
+ } finally {
9317
+ this.busy = null;
9318
+ send2.disabled = false;
9319
+ }
9320
+ }
9321
+ close() {
9322
+ this.busy?.abort();
9323
+ this.busy = null;
9324
+ for (const f of this.off) f();
9325
+ this.off = [];
9326
+ this.popup?.remove();
9327
+ this.popup = null;
9328
+ this.log = null;
9329
+ }
9330
+ };
9331
+
7467
9332
  // src/grid.ts
7468
9333
  var autoRowHeight = (size) => Math.round(size * 2.75);
7469
9334
  var autoHeaderHeight = (size) => Math.round(size * 3.1);
@@ -7508,11 +9373,34 @@ var Grid = class {
7508
9373
  /** 0-based page and rows per page (pagination only). */
7509
9374
  this.page = 0;
7510
9375
  this.pageSize = DEFAULT_PAGE_SIZE;
9376
+ /** What formulas see: every source row, and computed columns by id. */
9377
+ this.formulaScope = {
9378
+ rows: () => this.store.state.data,
9379
+ version: () => this.editVersion,
9380
+ computed: (name) => {
9381
+ const def = this.columns.getNode(name)?.def;
9382
+ return def && typeof def.formula === "string" && def.formula.startsWith("=") ? def.formula : void 0;
9383
+ }
9384
+ };
9385
+ this.cfRules = [];
9386
+ this.formats = new ConditionalFormatter({
9387
+ rules: () => this.cfRules,
9388
+ rows: () => this.rowModel.dataRows().map((n) => n.data),
9389
+ value: (colId, row) => {
9390
+ const def = this.columns.getNode(colId)?.def;
9391
+ return def ? getCellValue(def, row) : void 0;
9392
+ },
9393
+ version: () => this.editVersion,
9394
+ formula: (f, row) => evaluateFormula(f, row, /* @__PURE__ */ new Set(), 0, /* @__PURE__ */ new Map(), this.formulaScope)
9395
+ });
9396
+ this.formatDialog = new FormatDialog(this);
9397
+ this.assistantPanel = new AssistantPanel(this);
7511
9398
  // ── pagination and server rows ──────────────────────────────
7512
9399
  /** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
7513
9400
  /** showOverlay: an overlay the app asked for, shown whatever the row count. */
7514
9401
  this.forcedOverlay = null;
7515
9402
  this.filterBuilder = new FilterBuilder(this);
9403
+ /** `${rowId}\u0000${colId}` → when the flash ends and which way a number moved. */
7516
9404
  this.flashing = /* @__PURE__ */ new Map();
7517
9405
  this.flashTimer = 0;
7518
9406
  this.txQueue = [];
@@ -7552,6 +9440,7 @@ var Grid = class {
7552
9440
  });
7553
9441
  this.sourceDefs = Array.isArray(options.columns) ? options.columns : [];
7554
9442
  this.columns = new ColumnModel(this.withSelect(this.sourceDefs));
9443
+ this.bindFormulas();
7555
9444
  const pg = options.pagination;
7556
9445
  if (pg && typeof pg === "object" && pg.pageSize !== void 0) {
7557
9446
  if (Number.isInteger(pg.pageSize) && pg.pageSize > 0) this.pageSize = pg.pageSize;
@@ -7573,6 +9462,7 @@ var Grid = class {
7573
9462
  collator: () => this.collator()
7574
9463
  });
7575
9464
  this.groupBy = this.validGroupBy(options.groupBy ?? []);
9465
+ if (Array.isArray(options.conditionalFormats)) this.cfRules = options.conditionalFormats.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
7576
9466
  this.defaultExpanded = options.groupDefaultExpanded ?? 0;
7577
9467
  if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
7578
9468
  this.updateRows();
@@ -7588,6 +9478,7 @@ var Grid = class {
7588
9478
  });
7589
9479
  this.density = this.densityOf(options.density);
7590
9480
  this.renderer.applyTheme();
9481
+ if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
7591
9482
  this.renderer.applyFont(this.font.size, this.font.family);
7592
9483
  this.menu = new ColumnMenu(this);
7593
9484
  this.chooser = new ColumnChooserDialog(this);
@@ -7614,6 +9505,8 @@ var Grid = class {
7614
9505
  () => this.groupPanel?.destroy(),
7615
9506
  () => this.charts.destroy(),
7616
9507
  () => this.filterBuilder.close(),
9508
+ () => this.formatDialog.close(),
9509
+ () => this.assistantPanel.close(),
7617
9510
  attachTooltips(this)
7618
9511
  );
7619
9512
  if (typeof ResizeObserver !== "undefined") {
@@ -7629,6 +9522,45 @@ var Grid = class {
7629
9522
  this.api = this.createApi();
7630
9523
  liveGrids.set(this.renderer.root, this);
7631
9524
  }
9525
+ /** The column definitions as given (setColumns), without the system columns. */
9526
+ columnDefs() {
9527
+ return [...this.sourceDefs];
9528
+ }
9529
+ summaryOptions() {
9530
+ return this.summaryOpts ? JSON.parse(JSON.stringify(this.summaryOpts)) : null;
9531
+ }
9532
+ setConditionalFormats(list) {
9533
+ if (!Array.isArray(list)) {
9534
+ warn("setConditionalFormats: formats must be an array");
9535
+ return;
9536
+ }
9537
+ this.cfRules = list.filter((r) => r && typeof r === "object" && Array.isArray(r.columns)).map((r) => ({ ...r, columns: [...r.columns] }));
9538
+ this.renderer.invalidate({ rows: true });
9539
+ this.events.emit("conditionalFormatsChange", { formats: this.getConditionalFormats() });
9540
+ }
9541
+ getConditionalFormats() {
9542
+ return JSON.parse(JSON.stringify(this.cfRules));
9543
+ }
9544
+ /** Conditional format of one cell, or null. */
9545
+ cellFormat(row, colId, value) {
9546
+ return this.cfRules.length ? this.formats.cell(colId, row, value) : null;
9547
+ }
9548
+ bindFormulas() {
9549
+ const defs = [];
9550
+ const walk = (list) => {
9551
+ for (const d of list) {
9552
+ if (!d || typeof d !== "object") continue;
9553
+ defs.push(d);
9554
+ if (Array.isArray(d.children)) walk(d.children);
9555
+ }
9556
+ };
9557
+ walk(this.sourceDefs);
9558
+ for (const st of this.columns.getState()) {
9559
+ const d = this.columns.getNode(st.colId)?.def;
9560
+ if (d) defs.push(d);
9561
+ }
9562
+ setFormulaScope(defs, this.formulaScope);
9563
+ }
7632
9564
  collator() {
7633
9565
  const code = this.store.state.locale.code;
7634
9566
  if (this.collatorCache?.code !== code) {
@@ -7766,17 +9698,35 @@ var Grid = class {
7766
9698
  isFlashing(rowId, colId) {
7767
9699
  return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
7768
9700
  }
9701
+ /** Classes of a flashing cell ('' when it is not flashing). */
9702
+ flashClass(rowId, colId) {
9703
+ if (!this.flashing.size) return "";
9704
+ const f = this.flashing.get(`${rowId}\0${colId}`);
9705
+ return f ? f.dir ? ` fg-cell-flash fg-cell-flash-${f.dir}` : " fg-cell-flash" : "";
9706
+ }
9707
+ flashMs() {
9708
+ const d = this.options.flashDuration;
9709
+ return typeof d === "number" && d > 0 ? d : 1e3;
9710
+ }
7769
9711
  flash(keys) {
7770
- const until = Date.now() + 1e3;
7771
- for (const k of keys) this.flashing.set(k, until);
9712
+ const ms = this.flashMs();
9713
+ const until = Date.now() + ms;
9714
+ for (const [k, dir] of keys) this.flashing.set(k, { until, dir });
7772
9715
  if (this.flashTimer) return;
7773
9716
  const tick = () => {
7774
9717
  const now = Date.now();
7775
- for (const [k, t] of this.flashing) if (t <= now) this.flashing.delete(k);
7776
- this.renderer.invalidate({ rows: true });
7777
- this.flashTimer = this.flashing.size && !this.destroyed ? window.setTimeout(tick, 250) : 0;
9718
+ const ended = /* @__PURE__ */ new Set();
9719
+ let next = Infinity;
9720
+ for (const [k, f] of this.flashing) {
9721
+ if (f.until <= now) {
9722
+ this.flashing.delete(k);
9723
+ ended.add(k.slice(0, k.indexOf("\0")));
9724
+ } else next = Math.min(next, f.until);
9725
+ }
9726
+ if (ended.size) this.renderer.invalidateRowIds(ended);
9727
+ this.flashTimer = this.flashing.size && !this.destroyed ? window.setTimeout(tick, Math.max(16, next - now)) : 0;
7778
9728
  };
7779
- this.flashTimer = window.setTimeout(tick, 1e3);
9729
+ this.flashTimer = window.setTimeout(tick, ms);
7780
9730
  }
7781
9731
  applyTransaction(tx) {
7782
9732
  if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
@@ -7791,32 +9741,55 @@ var Grid = class {
7791
9741
  const f = this.columns.getNode(st.colId)?.def.field;
7792
9742
  if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7793
9743
  }
7794
- const flashKeys = [];
9744
+ const flashKeys = /* @__PURE__ */ new Map();
9745
+ const changed = { fields: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set() };
7795
9746
  let next = this.store.state.data;
7796
9747
  const present = new Set(next);
7797
9748
  const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
7798
9749
  for (const tx of list) {
7799
- if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField);
9750
+ if (tx && typeof tx === "object") next = this.txStep(tx, next, { present, byId }, result, flashKeys, colsByField, changed);
7800
9751
  }
7801
9752
  if (!result.added.length && !result.updated.length && !result.removed.length) {
7802
9753
  if (result.notFound.length) this.events.emit("transactionApplied", result);
7803
9754
  return result;
7804
9755
  }
7805
9756
  this.editVersion++;
7806
- this.renderer.invalidateHeights();
7807
- this.keepRange = true;
7808
- try {
7809
- this.replaceData(next === this.store.state.data ? [...next] : next);
7810
- } finally {
7811
- this.keepRange = false;
9757
+ if (next === this.store.state.data && !this.viewReads(changed.fields)) {
9758
+ if (this.options.autoRowHeight || this.options.getRowHeight) this.renderer.invalidateHeights();
9759
+ this.refreshSummary();
9760
+ this.finder.refresh();
9761
+ if (this.columns.displayed().some((c) => c.def.merge || c.def.rowSpan || c.def.formula || c.def.formulas) || this.formats.dependsOnOtherRows()) {
9762
+ this.renderer.invalidate({ rows: true });
9763
+ } else this.renderer.invalidateRowIds(changed.ids);
9764
+ } else {
9765
+ this.renderer.invalidateHeights();
9766
+ this.keepRange = true;
9767
+ try {
9768
+ this.replaceData(next === this.store.state.data ? [...next] : next);
9769
+ } finally {
9770
+ this.keepRange = false;
9771
+ }
7812
9772
  }
7813
9773
  this.editor.sync();
7814
- if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
9774
+ if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
7815
9775
  this.events.emit("transactionApplied", result);
7816
9776
  return result;
7817
9777
  }
7818
9778
  /** One transaction against `data` (remove → update → add); returns the new array. */
7819
- txStep(tx, data, index, result, flashKeys, colsByField) {
9779
+ /** Whether rows changed in `fields` could filter, sort, group or move differently. */
9780
+ viewReads(fields) {
9781
+ if (!fields.size) return false;
9782
+ const st = this.store.state;
9783
+ if (this.pivotCfg || this.options.treeData || st.quickFilter || st.advancedFilter || this.groupBy.length) return true;
9784
+ const ids = [...st.sort.map((x) => x.colId), ...Object.keys(st.filters)];
9785
+ return ids.some((id) => {
9786
+ const def = this.columns.getNode(id)?.def;
9787
+ if (!def) return false;
9788
+ if (def.valueGetter || def.formulas || def.formula) return true;
9789
+ return def.field != null && fields.has(def.field);
9790
+ });
9791
+ }
9792
+ txStep(tx, data, index, result, flashKeys, colsByField, changed) {
7820
9793
  const getId = this.options.getRowId;
7821
9794
  const { present, byId } = index;
7822
9795
  const keyOf = (r) => {
@@ -7841,6 +9814,20 @@ var Grid = class {
7841
9814
  this.tracker.originals.delete(row);
7842
9815
  } else if (!row) result.notFound.push(r);
7843
9816
  }
9817
+ const update = (r, k, row) => {
9818
+ const orig = this.tracker.originals.get(row);
9819
+ for (const [field, value] of Object.entries(r)) {
9820
+ if (orig) orig[field] = value;
9821
+ const old = row[field];
9822
+ if (Object.is(old, value)) continue;
9823
+ row[field] = value;
9824
+ changed.fields.add(field);
9825
+ changed.ids.add(k);
9826
+ const dir = typeof old === "number" && typeof value === "number" ? value > old ? "up" : "down" : "";
9827
+ for (const colId of colsByField.get(field) ?? []) flashKeys.set(`${k}\0${colId}`, dir);
9828
+ }
9829
+ result.updated.push(row);
9830
+ };
7844
9831
  for (const r of tx.update ?? []) {
7845
9832
  const k = keyOf(r);
7846
9833
  const row = k === null ? void 0 : byId.get(k);
@@ -7848,16 +9835,16 @@ var Grid = class {
7848
9835
  result.notFound.push(r);
7849
9836
  continue;
7850
9837
  }
7851
- const orig = this.tracker.originals.get(row);
7852
- for (const [field, value] of Object.entries(r)) {
7853
- if (orig) orig[field] = value;
7854
- if (Object.is(row[field], value)) continue;
7855
- row[field] = value;
7856
- for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
7857
- }
7858
- result.updated.push(row);
9838
+ update(r, k, row);
7859
9839
  }
7860
- const added = cleanRows(Array.isArray(tx.add) ? tx.add : []);
9840
+ const fresh = Array.isArray(tx.add) ? [...tx.add] : [];
9841
+ for (const r of Array.isArray(tx.upsert) ? tx.upsert : []) {
9842
+ const k = keyOf(r);
9843
+ const row = k === null ? void 0 : byId.get(k);
9844
+ if (row && !gone.has(row)) update(r, k, row);
9845
+ else if (r && typeof r === "object") fresh.push(r);
9846
+ }
9847
+ const added = cleanRows(fresh);
7861
9848
  result.added.push(...added);
7862
9849
  for (const r of added) {
7863
9850
  present.add(r);
@@ -7873,13 +9860,16 @@ var Grid = class {
7873
9860
  if (!tx || typeof tx !== "object") return;
7874
9861
  this.txQueue.push(tx);
7875
9862
  if (this.txTimer) return;
7876
- this.txTimer = window.setTimeout(() => {
7877
- this.txTimer = 0;
7878
- const queue = this.txQueue;
7879
- this.txQueue = [];
7880
- if (this.destroyed || !queue.length) return;
7881
- this.applyTransactions(queue);
7882
- }, 50);
9863
+ const wait = this.options.asyncTransactionWait;
9864
+ this.txTimer = window.setTimeout(() => this.flushAsyncTransactions(), typeof wait === "number" && wait >= 0 ? wait : 50);
9865
+ }
9866
+ flushAsyncTransactions() {
9867
+ clearTimeout(this.txTimer);
9868
+ this.txTimer = 0;
9869
+ const queue = this.txQueue;
9870
+ this.txQueue = [];
9871
+ if (this.destroyed || !queue.length) return;
9872
+ this.applyTransactions(queue);
7883
9873
  }
7884
9874
  setAdvancedFilter(filter) {
7885
9875
  const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
@@ -7908,6 +9898,7 @@ var Grid = class {
7908
9898
  expanded: Object.fromEntries(expanded),
7909
9899
  selected: [...s.selected]
7910
9900
  };
9901
+ if (this.cfRules.length) state.conditionalFormats = this.getConditionalFormats();
7911
9902
  if (this.options.pagination) {
7912
9903
  state.page = this.page;
7913
9904
  state.pageSize = this.pageSize;
@@ -7945,6 +9936,7 @@ var Grid = class {
7945
9936
  }
7946
9937
  if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
7947
9938
  if (want("page") && typeof state.page === "number") this.setPage(state.page);
9939
+ if (want("conditionalFormats") && Array.isArray(state.conditionalFormats)) this.setConditionalFormats(state.conditionalFormats);
7948
9940
  if (want("selected") && Array.isArray(state.selected)) {
7949
9941
  const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
7950
9942
  this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
@@ -9194,6 +11186,7 @@ var Grid = class {
9194
11186
  return;
9195
11187
  }
9196
11188
  this.columns.setDefs(this.withSelect(defs));
11189
+ this.bindFormulas();
9197
11190
  const known = (id) => !!this.columns.getNode(id);
9198
11191
  const groupBy = this.groupBy.filter(known);
9199
11192
  if (groupBy.length !== this.groupBy.length) {
@@ -9271,6 +11264,12 @@ var Grid = class {
9271
11264
  getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
9272
11265
  print: guard((o) => printHtml(buildPrintHtml(this, o))),
9273
11266
  getPrintHtml: guard((o) => buildPrintHtml(this, o)),
11267
+ getPdf: guard((o) => renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality })),
11268
+ exportPdf: guard((name = "export.pdf", o) => {
11269
+ const bytes = renderPdf(printModel(this, o), browserSurface, { scale: o?.resolution, quality: o?.quality });
11270
+ if (bytes) download(name, [bytes], "application/pdf");
11271
+ else warn("exportPdf: this browser cannot draw on a canvas");
11272
+ }),
9274
11273
  setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
9275
11274
  setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
9276
11275
  getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
@@ -9278,6 +11277,12 @@ var Grid = class {
9278
11277
  isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
9279
11278
  applyTransaction: guard((tx) => this.applyTransaction(tx)),
9280
11279
  applyTransactionAsync: guard((tx) => this.applyTransactionAsync(tx)),
11280
+ flushAsyncTransactions: guard(() => this.flushAsyncTransactions()),
11281
+ setConditionalFormats: guard((f) => this.setConditionalFormats(f)),
11282
+ getConditionalFormats: guard(() => this.getConditionalFormats()),
11283
+ openConditionalFormats: guard((colId) => this.formatDialog.open(colId)),
11284
+ askAssistant: guard((prompt) => askAssistant(this, prompt, this.options.assistant)),
11285
+ openAssistant: guard(() => this.assistantPanel.open()),
9281
11286
  getState: guard(() => this.getState()),
9282
11287
  setState: guard((s, o) => this.setState(s, o)),
9283
11288
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -9344,6 +11349,8 @@ var Grid = class {
9344
11349
  getPivot: guard(() => this.getPivot()),
9345
11350
  createChart: guard((o) => this.charts.create(o)),
9346
11351
  getCsv: guard((o) => buildCsv(this, o)),
11352
+ getJson: guard((o) => buildJson(this, o)),
11353
+ exportJson: guard((name = "export.json", o) => download(name, [buildJson(this, o)], "application/json;charset=utf-8")),
9347
11354
  exportCsv: guard((name = "export.csv", o) => download(name, ["\uFEFF", buildCsv(this, o)], "text/csv;charset=utf-8")),
9348
11355
  getXlsx: guard((o) => buildXlsx(this, o)),
9349
11356
  exportXlsx: guard((name = "export.xlsx", o) => download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")),
@@ -9441,13 +11448,13 @@ function saveChanges(grid, url, options = {}) {
9441
11448
  if (!grid.hasChanges()) return Promise.resolve(null);
9442
11449
  const { created, updated, deleted } = grid.getModifiedRows();
9443
11450
  const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
9444
- const run = (async () => {
11451
+ const run2 = (async () => {
9445
11452
  const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9446
11453
  grid.commitChanges({ rows, deleted });
9447
11454
  return json;
9448
11455
  })().finally(() => saving.delete(grid));
9449
- saving.set(grid, run);
9450
- return run;
11456
+ saving.set(grid, run2);
11457
+ return run2;
9451
11458
  }
9452
11459
  async function loadData(grid, url, options = {}) {
9453
11460
  const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
@@ -9459,15 +11466,186 @@ function csrfHeaders(doc = document) {
9459
11466
  const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
9460
11467
  return token ? { [header2]: token } : {};
9461
11468
  }
11469
+
11470
+ // src/live.ts
11471
+ var TX_KEYS = ["add", "update", "remove", "upsert"];
11472
+ function messageToTransaction(message) {
11473
+ let m = message;
11474
+ if (typeof m === "string") {
11475
+ if (!m.trim()) return null;
11476
+ m = JSON.parse(m);
11477
+ }
11478
+ if (Array.isArray(m)) return { upsert: m.filter((r) => r && typeof r === "object") };
11479
+ if (!m || typeof m !== "object") return null;
11480
+ const o = m;
11481
+ if (TX_KEYS.some((k) => Array.isArray(o[k]))) return o;
11482
+ if (Array.isArray(o.rows)) return { rows: o.rows };
11483
+ return null;
11484
+ }
11485
+ function connectLive(api, options) {
11486
+ const opts = options ?? {};
11487
+ let status = "connecting";
11488
+ let closed = false;
11489
+ let paused = false;
11490
+ let held = [];
11491
+ let socket = null;
11492
+ let timer = 0;
11493
+ let attempt = 0;
11494
+ const setStatus = (s) => {
11495
+ if (status === s) return;
11496
+ status = s;
11497
+ try {
11498
+ opts.onStatus?.(s);
11499
+ } catch {
11500
+ }
11501
+ };
11502
+ const apply = (tx) => {
11503
+ if ("rows" in tx && Array.isArray(tx.rows) && !TX_KEYS.some((k) => k in tx)) {
11504
+ api.flushAsyncTransactions();
11505
+ api.setData(tx.rows);
11506
+ } else if (opts.immediate) api.applyTransaction(tx);
11507
+ else api.applyTransactionAsync(tx);
11508
+ };
11509
+ const handle = (message) => {
11510
+ if (closed) return;
11511
+ let tx;
11512
+ try {
11513
+ tx = opts.parse ? opts.parse(message) : messageToTransaction(message);
11514
+ if (Array.isArray(tx)) tx = { upsert: tx };
11515
+ } catch (e) {
11516
+ if (opts.onError) opts.onError(e);
11517
+ else warn("connectLive: a message could not be read");
11518
+ return;
11519
+ }
11520
+ if (!tx || typeof tx !== "object") return;
11521
+ if (paused) held.push(tx);
11522
+ else apply(tx);
11523
+ };
11524
+ const retry = () => {
11525
+ if (closed || opts.reconnect === false) {
11526
+ setStatus("closed");
11527
+ return;
11528
+ }
11529
+ const max = typeof opts.maxDelay === "number" && opts.maxDelay > 0 ? opts.maxDelay : 3e4;
11530
+ const delay = Math.min(max, 500 * 2 ** Math.min(attempt++, 10)) * (0.8 + Math.random() * 0.4);
11531
+ setStatus("connecting");
11532
+ timer = window.setTimeout(open, delay);
11533
+ };
11534
+ const open = () => {
11535
+ if (closed) return;
11536
+ timer = 0;
11537
+ if (opts.poll) {
11538
+ void pollOnce();
11539
+ return;
11540
+ }
11541
+ const url = opts.url;
11542
+ if (!url) {
11543
+ warn("connectLive: give a url or a poll function");
11544
+ setStatus("error");
11545
+ return;
11546
+ }
11547
+ setStatus(paused ? "paused" : "connecting");
11548
+ try {
11549
+ if (/^wss?:/i.test(url)) {
11550
+ if (typeof WebSocket === "undefined") throw new Error("no WebSocket");
11551
+ const ws = new WebSocket(url, opts.protocols);
11552
+ socket = ws;
11553
+ ws.onopen = () => {
11554
+ attempt = 0;
11555
+ setStatus(paused ? "paused" : "open");
11556
+ };
11557
+ ws.onmessage = (e) => handle(e.data);
11558
+ ws.onerror = () => setStatus("error");
11559
+ ws.onclose = () => {
11560
+ if (socket === ws) socket = null;
11561
+ if (!closed) retry();
11562
+ };
11563
+ } else {
11564
+ if (typeof EventSource === "undefined") throw new Error("no EventSource");
11565
+ const es = new EventSource(url, { withCredentials: !!opts.withCredentials });
11566
+ socket = es;
11567
+ es.onopen = () => {
11568
+ attempt = 0;
11569
+ setStatus(paused ? "paused" : "open");
11570
+ };
11571
+ es.addEventListener(opts.event || "message", (e) => handle(e.data));
11572
+ es.onerror = () => {
11573
+ if (es.readyState === 2) {
11574
+ if (socket === es) socket = null;
11575
+ if (!closed) retry();
11576
+ } else setStatus("connecting");
11577
+ };
11578
+ }
11579
+ } catch {
11580
+ setStatus("error");
11581
+ retry();
11582
+ }
11583
+ };
11584
+ const pollOnce = async () => {
11585
+ if (closed) return;
11586
+ try {
11587
+ const result = await opts.poll();
11588
+ if (closed) return;
11589
+ attempt = 0;
11590
+ setStatus(paused ? "paused" : "open");
11591
+ handle(result);
11592
+ const every = typeof opts.interval === "number" && opts.interval > 0 ? opts.interval : 2e3;
11593
+ timer = window.setTimeout(pollOnce, every);
11594
+ } catch {
11595
+ if (closed) return;
11596
+ setStatus("error");
11597
+ retry();
11598
+ }
11599
+ };
11600
+ open();
11601
+ return {
11602
+ get status() {
11603
+ return status;
11604
+ },
11605
+ pause() {
11606
+ if (closed || paused) return;
11607
+ paused = true;
11608
+ setStatus("paused");
11609
+ },
11610
+ resume() {
11611
+ if (closed || !paused) return;
11612
+ paused = false;
11613
+ const list = held;
11614
+ held = [];
11615
+ for (const tx of list) apply(tx);
11616
+ setStatus(socket || opts.poll ? "open" : "connecting");
11617
+ },
11618
+ close() {
11619
+ if (closed) return;
11620
+ closed = true;
11621
+ clearTimeout(timer);
11622
+ socket?.close();
11623
+ socket = null;
11624
+ held = [];
11625
+ setStatus("closed");
11626
+ },
11627
+ send(data) {
11628
+ const ws = socket;
11629
+ if (!ws || typeof ws.send !== "function" || ws.readyState !== 1) return false;
11630
+ ws.send(data);
11631
+ return true;
11632
+ }
11633
+ };
11634
+ }
9462
11635
  export {
9463
11636
  buildWorkbook,
11637
+ connectLive,
9464
11638
  createGrid,
9465
11639
  csrfHeaders,
9466
11640
  en_default as en,
11641
+ evaluateFormula,
9467
11642
  exportWorkbook,
11643
+ formulaFunctions,
9468
11644
  loadData,
11645
+ messageToTransaction,
9469
11646
  parseCsv,
9470
11647
  readXlsx,
11648
+ registerFormulaFunction,
9471
11649
  registerLocale,
9472
11650
  renderChart,
9473
11651
  resolveLocale,