@freegridjs/core 1.0.1 → 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})`;
@@ -2582,6 +3114,7 @@ var en = {
2582
3114
  clearFilters: "Clear all filters",
2583
3115
  exportCsv: "Export CSV",
2584
3116
  exportExcel: "Export Excel",
3117
+ print: "Print\u2026",
2585
3118
  pivot: "Pivot",
2586
3119
  pivotMode: "Pivot mode",
2587
3120
  pivotRows: "Row groups",
@@ -2598,6 +3131,22 @@ var en = {
2598
3131
  chartCombo: "Bar and line chart",
2599
3132
  chartScatter: "Scatter chart",
2600
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
+ },
2601
3150
  ops: {
2602
3151
  contains: "Contains",
2603
3152
  equals: "Equals",
@@ -2702,9 +3251,9 @@ function nextFocus(key, mods, p, s) {
2702
3251
  case "End":
2703
3252
  return { rowIndex: mods.ctrl ? s.rows - 1 : p.rowIndex, colIndex: s.cols - 1 };
2704
3253
  case "Tab": {
2705
- const flat = p.rowIndex * s.cols + p.colIndex + (mods.shift ? -1 : 1);
2706
- if (flat < 0 || flat >= s.rows * s.cols) return null;
2707
- 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 };
2708
3257
  }
2709
3258
  default:
2710
3259
  return null;
@@ -2924,7 +3473,7 @@ function attachBody(grid) {
2924
3473
  const box = under?.getBoundingClientRect();
2925
3474
  let target = null;
2926
3475
  let position = "after";
2927
- let line;
3476
+ let line2;
2928
3477
  if (under && box) {
2929
3478
  const over = into.rowModel.getRow(Number(under.dataset.row));
2930
3479
  if (!over || over.kind === "group") return;
@@ -2933,17 +3482,17 @@ function attachBody(grid) {
2933
3482
  if (over.kind === "detail") position = "after";
2934
3483
  else if (into.options.treeData) position = y < 0.25 ? "before" : y > 0.75 ? "after" : "inside";
2935
3484
  else position = y < 0.5 ? "before" : "after";
2936
- line = position === "before" ? box.top : box.bottom;
3485
+ line2 = position === "before" ? box.top : box.bottom;
2937
3486
  } else {
2938
3487
  if (!point.closest(".fg-body")) return;
2939
3488
  const rows = intoRoot.querySelectorAll(".fg-body-pane-center .fg-row[data-row]");
2940
- 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;
2941
3490
  }
2942
3491
  if (!into.canDropRow(row, target, position, grid)) return;
2943
3492
  drop = { into, target, position };
2944
3493
  const top = intoRoot.getBoundingClientRect().top;
2945
3494
  marker.classList.toggle("fg-row-drop-inside", position === "inside");
2946
- 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`;
2947
3496
  marker.style.height = position === "inside" ? `${box.height}px` : "";
2948
3497
  intoRoot.append(marker);
2949
3498
  };
@@ -3527,6 +4076,7 @@ var SideBar = class {
3527
4076
  this.grid.renderer.invalidate({ header: true, rows: true });
3528
4077
  }
3529
4078
  syncTabs() {
4079
+ this.el.classList.toggle("fg-sidebar-open", this.isOpen);
3530
4080
  for (const [p, tab] of this.tabs) {
3531
4081
  const on = this.isOpen && p === this.current;
3532
4082
  tab.setAttribute("aria-expanded", String(on));
@@ -3640,9 +4190,13 @@ var ContextMenu = class {
3640
4190
  items.push(
3641
4191
  "separator",
3642
4192
  { label: loc.exportCsv, action: () => grid.api.exportCsv() },
3643
- { 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() }
3644
4197
  );
3645
4198
  if (grid.options.advancedFilter !== false && !grid.options.pivot) items.push("separator", { label: `${loc.advancedFilter}\u2026`, action: () => grid.filterBuilder.open() });
4199
+ items.push("separator", { label: loc.print, action: () => grid.api.print() });
3646
4200
  return items;
3647
4201
  }
3648
4202
  pasteFromClipboard() {
@@ -3665,11 +4219,11 @@ var ContextMenu = class {
3665
4219
  value: node && col && !isSystemCol(colId) ? getCellValue(col.def, node.data) : void 0,
3666
4220
  defaultItems: defaults
3667
4221
  };
3668
- const custom = grid.options.contextMenu;
4222
+ const custom2 = grid.options.contextMenu;
3669
4223
  let items = defaults;
3670
- if (typeof custom === "function") {
4224
+ if (typeof custom2 === "function") {
3671
4225
  try {
3672
- items = custom(params);
4226
+ items = custom2(params);
3673
4227
  } catch {
3674
4228
  items = defaults;
3675
4229
  }
@@ -3929,10 +4483,10 @@ var Finder = class {
3929
4483
  const bar = el("div", "fg-findbar", { role: "search" });
3930
4484
  const input = el("input", "fg-input", { type: "search", placeholder: loc.findPlaceholder, "aria-label": loc.findPlaceholder });
3931
4485
  this.countEl = el("span", "fg-findbar-count", { "aria-live": "polite" });
3932
- const btn = (label, text2, run) => {
4486
+ const btn = (label, text2, run2) => {
3933
4487
  const b = el("button", "fg-findbar-btn", { type: "button", "aria-label": label, title: label });
3934
4488
  b.textContent = text2;
3935
- b.addEventListener("click", run);
4489
+ b.addEventListener("click", run2);
3936
4490
  return b;
3937
4491
  };
3938
4492
  bar.append(
@@ -3968,7 +4522,7 @@ var Finder = class {
3968
4522
  };
3969
4523
 
3970
4524
  // src/fill.ts
3971
- var DAY = 864e5;
4525
+ var DAY2 = 864e5;
3972
4526
  function fillSeries(source, count) {
3973
4527
  if (!source.length || count <= 0) return [];
3974
4528
  const repeat = () => Array.from({ length: count }, (_, i) => source[i % source.length]);
@@ -3980,9 +4534,9 @@ function fillSeries(source, count) {
3980
4534
  const last2 = nums[nums.length - 1];
3981
4535
  return Array.from({ length: count }, (_, i) => Math.round((last2 + step * (i + 1)) * 1e12) / 1e12);
3982
4536
  }
3983
- 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);
3984
4538
  if (dates.every(Boolean) && source.length >= 2) {
3985
- const times = dates.map((d) => Math.round(d.getTime() / DAY));
4539
+ const times = dates.map((d) => Math.round(d.getTime() / DAY2));
3986
4540
  const step = times[1] - times[0];
3987
4541
  if (times.every((t, i) => i === 0 || t - times[i - 1] === step)) {
3988
4542
  const lastDate = dates[dates.length - 1];
@@ -4117,7 +4671,7 @@ function parseDate(text2, previous) {
4117
4671
  let m = /^(\d{4})\s*[-./년]\s*(\d{1,2})\s*[-./월]\s*(\d{1,2})\s*[.일]?$/.exec(s);
4118
4672
  if (m) date = validDate(+m[1], +m[2], +m[3]);
4119
4673
  else if (m = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(s)) date = validDate(+m[3], +m[1], +m[2]);
4120
- 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);
4121
4675
  if (!date) return { ok: false };
4122
4676
  return { ok: true, value: previous instanceof Date ? date : toDateKey2(date) };
4123
4677
  }
@@ -4141,16 +4695,16 @@ function parseValue(def, text2, previous) {
4141
4695
 
4142
4696
  // src/editing/editor.ts
4143
4697
  var normalizeOptions = (opts) => opts.map((o) => o !== null && typeof o === "object" && "value" in o ? o : { value: o, label: String(o), disabled: false });
4144
- var pad2 = (n) => String(n).padStart(2, "0");
4698
+ var pad3 = (n) => String(n).padStart(2, "0");
4145
4699
  function localDateTime(v) {
4146
4700
  const d = v instanceof Date ? v : typeof v === "string" && v ? new Date(v) : null;
4147
4701
  if (!d || Number.isNaN(d.getTime())) return "";
4148
- 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())}`;
4149
4703
  }
4150
4704
  function timeOf(v) {
4151
- if (v instanceof Date) return `${pad2(v.getHours())}:${pad2(v.getMinutes())}`;
4705
+ if (v instanceof Date) return `${pad3(v.getHours())}:${pad3(v.getMinutes())}`;
4152
4706
  const m = typeof v === "string" ? /(\d{1,2}):(\d{2})/.exec(v) : null;
4153
- return m ? `${pad2(Number(m[1]))}:${m[2]}` : "";
4707
+ return m ? `${pad3(Number(m[1]))}:${m[2]}` : "";
4154
4708
  }
4155
4709
  var CellEditor = class {
4156
4710
  constructor(grid) {
@@ -4225,20 +4779,20 @@ var CellEditor = class {
4225
4779
  grid.setFocus(rowIndex, colId);
4226
4780
  renderer.ensureVisible(rowIndex, colId);
4227
4781
  renderer.render();
4228
- const wrap = el("div", "fg-editor");
4229
- this.place(wrap, rowIndex, colId);
4782
+ const wrap2 = el("div", "fg-editor");
4783
+ this.place(wrap2, rowIndex, colId);
4230
4784
  let control;
4231
4785
  let read;
4232
4786
  let dispose = () => {
4233
4787
  };
4234
4788
  const spec = def.editor;
4235
4789
  if (typeof spec === "function") {
4236
- const custom = spec({ value, row, column: def, initialText });
4237
- control = custom.el;
4238
- read = () => ({ value: custom.getValue() });
4239
- dispose = () => custom.destroy?.();
4240
- wrap.append(control);
4241
- 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?.());
4242
4796
  } else if (spec?.type === "select") {
4243
4797
  const options = normalizeOptions(typeof spec.options === "function" ? spec.options(row) : spec.options);
4244
4798
  const select = el("select", "fg-editor-control");
@@ -4252,7 +4806,7 @@ var CellEditor = class {
4252
4806
  select.selectedIndex = Math.max(0, options.findIndex((o) => Object.is(o.value, value)));
4253
4807
  control = select;
4254
4808
  read = () => ({ value: options[select.selectedIndex]?.value ?? null });
4255
- wrap.append(select);
4809
+ wrap2.append(select);
4256
4810
  } else if (spec?.type === "time" || spec?.type === "datetime" || !spec && def.type === "date" && def.format === "datetime") {
4257
4811
  const isTime = spec?.type === "time";
4258
4812
  const input = el("input", "fg-editor-control", { type: isTime ? "time" : "datetime-local" });
@@ -4263,7 +4817,7 @@ var CellEditor = class {
4263
4817
  if (isTime) return { value: input.value };
4264
4818
  return { value: value instanceof Date ? new Date(input.value) : input.value };
4265
4819
  };
4266
- wrap.append(input);
4820
+ wrap2.append(input);
4267
4821
  } else {
4268
4822
  const textarea = spec?.type === "textarea";
4269
4823
  const input = textarea ? el("textarea", "fg-editor-control") : el("input", "fg-editor-control");
@@ -4275,11 +4829,11 @@ var CellEditor = class {
4275
4829
  input.value = initialText ?? start;
4276
4830
  control = input;
4277
4831
  read = () => ({ text: input.value });
4278
- wrap.append(input);
4832
+ wrap2.append(input);
4279
4833
  }
4280
4834
  control.setAttribute("aria-label", def.header ?? def.field ?? colId);
4281
4835
  const msg = el("div", "fg-editor-msg", { role: "alert" });
4282
- wrap.append(msg);
4836
+ wrap2.append(msg);
4283
4837
  const onKey = (e) => {
4284
4838
  e.stopPropagation();
4285
4839
  if (e.key === "Escape") {
@@ -4295,26 +4849,26 @@ var CellEditor = class {
4295
4849
  }
4296
4850
  };
4297
4851
  const onFocusOut = (e) => {
4298
- 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);
4299
4853
  };
4300
4854
  const onScroll = () => this.commit(null);
4301
- wrap.addEventListener("keydown", onKey);
4302
- wrap.addEventListener("focusout", onFocusOut);
4855
+ wrap2.addEventListener("keydown", onKey);
4856
+ wrap2.addEventListener("focusout", onFocusOut);
4303
4857
  renderer.scroller.addEventListener("scroll", onScroll, { passive: true });
4304
- renderer.root.append(wrap);
4858
+ renderer.root.append(wrap2);
4305
4859
  this.cur = {
4306
4860
  rowIndex,
4307
4861
  colId,
4308
4862
  row,
4309
4863
  def,
4310
- wrap,
4864
+ wrap: wrap2,
4311
4865
  read,
4312
4866
  dispose: () => {
4313
- wrap.removeEventListener("keydown", onKey);
4314
- wrap.removeEventListener("focusout", onFocusOut);
4867
+ wrap2.removeEventListener("keydown", onKey);
4868
+ wrap2.removeEventListener("focusout", onFocusOut);
4315
4869
  renderer.scroller.removeEventListener("scroll", onScroll);
4316
4870
  dispose();
4317
- wrap.remove();
4871
+ wrap2.remove();
4318
4872
  }
4319
4873
  };
4320
4874
  if (typeof spec !== "function") {
@@ -4325,15 +4879,15 @@ var CellEditor = class {
4325
4879
  return true;
4326
4880
  }
4327
4881
  /** Put the editor over its cell; false when the cell is not rendered. */
4328
- place(wrap, rowIndex, colId) {
4882
+ place(wrap2, rowIndex, colId) {
4329
4883
  const { renderer } = this.grid;
4330
4884
  const cellEl = renderer.root.querySelector(`.fg-row[data-row="${rowIndex}"] .fg-cell[data-col="${CSS.escape(colId)}"]`);
4331
4885
  const rootBox = renderer.root.getBoundingClientRect();
4332
4886
  const box = cellEl?.getBoundingClientRect();
4333
- wrap.style.left = `${(box?.left ?? rootBox.left) - rootBox.left}px`;
4334
- wrap.style.top = `${(box?.top ?? rootBox.top) - rootBox.top}px`;
4335
- wrap.style.width = `${box?.width || this.grid.columns.getColumn(colId)?.width}px`;
4336
- 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`;
4337
4891
  return !!cellEl;
4338
4892
  }
4339
4893
  /** Display index of a row object, or -1 when it is no longer shown. */
@@ -4677,6 +5231,662 @@ function attachHeader(grid) {
4677
5231
  };
4678
5232
  }
4679
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
+
5838
+ // src/rows/sort.ts
5839
+ function compareNonBlank(a, b, collator) {
5840
+ if (typeof a === "number" && typeof b === "number") return a - b;
5841
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
5842
+ if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
5843
+ return collator.compare(String(a), String(b));
5844
+ }
5845
+ function normalize(def, v) {
5846
+ if (isBlank(v)) return null;
5847
+ if (def.type === "number") {
5848
+ const n = typeof v === "number" ? v : Number(v);
5849
+ return Number.isNaN(n) ? null : n;
5850
+ }
5851
+ if (def.type === "date") return toDate2(v)?.getTime() ?? null;
5852
+ return v;
5853
+ }
5854
+ function compareValues(a, b, collator) {
5855
+ const ba = isBlank(a), bb = isBlank(b);
5856
+ if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
5857
+ return compareNonBlank(a, b, collator);
5858
+ }
5859
+ function sortNodes(nodes, sort, resolve, collator) {
5860
+ const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
5861
+ if (!keys.length) return nodes;
5862
+ const values = keys.map((k) => nodes.map((n) => {
5863
+ const v = getCellValue(k.def, n.data);
5864
+ return k.def.comparator ? v : normalize(k.def, v);
5865
+ }));
5866
+ const order = nodes.map((_, i) => i);
5867
+ order.sort((i, j) => {
5868
+ for (let k = 0; k < keys.length; k++) {
5869
+ const { def, mul } = keys[k];
5870
+ const a = values[k][i], b = values[k][j];
5871
+ let c;
5872
+ if (def.comparator) {
5873
+ try {
5874
+ c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
5875
+ } catch {
5876
+ warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
5877
+ c = 0;
5878
+ }
5879
+ } else {
5880
+ const ba = isBlank(a), bb = isBlank(b);
5881
+ c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
5882
+ }
5883
+ if (c) return c;
5884
+ }
5885
+ return i - j;
5886
+ });
5887
+ return order.map((i) => nodes[i]);
5888
+ }
5889
+
4680
5890
  // src/export/csv.ts
4681
5891
  var FORMULA = /^[=+\-@\t\r]/;
4682
5892
  function toCsv(rows, sep = ",") {
@@ -4689,8 +5899,8 @@ function toCsv(rows, sep = ",") {
4689
5899
  return rows.map((r) => r.map(cell).join(sep)).join("\r\n");
4690
5900
  }
4691
5901
  function detectSeparator(text2) {
4692
- const line = text2.slice(0, text2.search(/\r?\n|$/));
4693
- 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;
4694
5904
  const tabs = count(" "), semis = count(";"), commas = count(",");
4695
5905
  if (tabs && tabs >= commas && tabs >= semis) return " ";
4696
5906
  if (semis > commas) return ";";
@@ -4847,12 +6057,12 @@ function colName(i) {
4847
6057
  return s;
4848
6058
  }
4849
6059
  var EPOCH = Date.UTC(1899, 11, 30);
4850
- var DAY2 = 864e5;
6060
+ var DAY3 = 864e5;
4851
6061
  function toSerial(d) {
4852
- 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;
4853
6063
  }
4854
6064
  function fromSerial(n) {
4855
- const u = new Date(Math.round(n * DAY2) + EPOCH);
6065
+ const u = new Date(Math.round(n * DAY3) + EPOCH);
4856
6066
  return new Date(u.getUTCFullYear(), u.getUTCMonth(), u.getUTCDate(), u.getUTCHours(), u.getUTCMinutes(), u.getUTCSeconds());
4857
6067
  }
4858
6068
  var BAD_XML = /[\u0000-\u0008\u000b\u000c\u000e-\u001f￾￿]/g;
@@ -4884,7 +6094,7 @@ function writeXlsx(sheets) {
4884
6094
  ];
4885
6095
  const fillIndex = /* @__PURE__ */ new Map([["FFF2F4F7", 2]]);
4886
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()}` : "";
4887
- const fontOf = (bold, color) => {
6097
+ const fontOf2 = (bold, color) => {
4888
6098
  const key = `${bold ? 1 : 0}|${color}`;
4889
6099
  let i = fontIndex.get(key);
4890
6100
  if (i === void 0) {
@@ -4918,7 +6128,7 @@ function writeXlsx(sheets) {
4918
6128
  customFmts.push(fmt);
4919
6129
  fmtIds.set(fmt, id);
4920
6130
  }
4921
- const fontId = fontOf(bold, color);
6131
+ const fontId = fontOf2(bold, color);
4922
6132
  const fillId = fillOf(fill);
4923
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"' : "");
4924
6134
  i = xfs.length;
@@ -4971,9 +6181,13 @@ function writeXlsx(sheets) {
4971
6181
  },
4972
6182
  {
4973
6183
  name: "xl/workbook.xml",
4974
- 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) => {
4975
6185
  const w = Math.max(0, ...s.rows.map((r) => r.length));
4976
- 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;
4977
6191
  }).join("")}</definedNames>` : "") + "</workbook>"
4978
6192
  },
4979
6193
  {
@@ -5017,11 +6231,11 @@ async function readXlsx(input) {
5017
6231
  const shared = parseXml(files.get("xl/sharedStrings.xml"));
5018
6232
  const strings = shared ? tags(shared, "si").map((si) => tags(si, "t").map((t) => t.textContent ?? "").join("")) : [];
5019
6233
  const stylesDoc = parseXml(files.get("xl/styles.xml"));
5020
- 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") ?? ""]) : []);
5021
6235
  const cellXfs = stylesDoc ? tags(stylesDoc, "cellXfs")[0] : void 0;
5022
6236
  const dateStyle = (cellXfs ? [...cellXfs.children].filter((x) => x.localName === "xf") : []).map((xf) => {
5023
6237
  const id = Number(xf.getAttribute("numFmtId") ?? 0);
5024
- 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));
5025
6239
  });
5026
6240
  const sheets = [];
5027
6241
  for (const s of tags(workbook, "sheet")) {
@@ -5087,10 +6301,70 @@ var header = (def, id) => def.header ?? def.field ?? id;
5087
6301
  var ALIGNS = ["left", "center", "right"];
5088
6302
  var PLAIN_NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
5089
6303
  var escHtml = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
5090
- function buildPrintHtml(grid, opts = {}) {
6304
+ var PAPERS = ["A4", "A3", "A5", "B4", "B5", "letter", "legal"];
6305
+ var PORTRAIT_WIDTH = 760;
6306
+ var cssStr = (s) => `"${s.replace(/[\\"<\n\r]/g, (c) => c === "\n" ? "\\A " : c === "\r" ? "" : `\\${c.charCodeAt(0).toString(16)} `)}"`;
6307
+ var hexOf = (argb) => argb ? `#${argb.slice(2)}` : "";
6308
+ function printLines(grid, rows, groupBy) {
6309
+ const out = [];
6310
+ let n = 0;
6311
+ const sort = grid.store.state.sort;
6312
+ const collator = grid.collator();
6313
+ const walk = (list, level) => {
6314
+ if (level >= groupBy.length) {
6315
+ for (const row of list) out.push({ kind: "row", row, n: ++n });
6316
+ return;
6317
+ }
6318
+ const colId = groupBy[level];
6319
+ const def = grid.columns.getNode(colId)?.def;
6320
+ if (!def) return walk(list, level + 1);
6321
+ const buckets = /* @__PURE__ */ new Map();
6322
+ for (const r of list) {
6323
+ const v = getCellValue(def, r);
6324
+ const k = isBlank(v) ? "" : setKey(v);
6325
+ let b = buckets.get(k);
6326
+ if (!b) buckets.set(k, b = { value: v, rows: [] });
6327
+ b.rows.push(r);
6328
+ }
6329
+ const dir = sort.find((x) => x.colId === colId)?.dir === "desc" ? -1 : 1;
6330
+ for (const b of [...buckets.values()].sort((a, c) => compareValues(a.value, c.value, collator) * dir)) {
6331
+ out.push({ kind: "group", colId, value: b.value, rows: b.rows, level });
6332
+ walk(b.rows, level + 1);
6333
+ }
6334
+ };
6335
+ walk(rows, 0);
6336
+ return out;
6337
+ }
6338
+ var PRINT_CSS = `
6339
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
6340
+ body { font: 10.5px/1.45 system-ui, 'Pretendard', 'Malgun Gothic', 'Apple SD Gothic Neo', sans-serif; color: #111827; margin: 0; }
6341
+ header { display: flex; justify-content: space-between; align-items: flex-end; gap: 16px; margin: 0 0 10px; padding-bottom: 8px; border-bottom: 2px solid #111827; }
6342
+ h1 { font-size: 17px; margin: 0; letter-spacing: -.01em; }
6343
+ .sub { margin: 3px 0 0; color: #4b5563; font-size: 11px; }
6344
+ .printed { margin: 0; color: #6b7280; font-size: 9.5px; white-space: nowrap; }
6345
+ table { border-collapse: collapse; width: 100%; }
6346
+ table.fit { table-layout: fixed; }
6347
+ th, td { padding: 4px 7px; border-bottom: 1px solid #e5e7eb; vertical-align: top; overflow-wrap: anywhere; font-variant-numeric: tabular-nums; }
6348
+ th { background: #f3f4f6; color: #374151; font-weight: 600; text-align: left; vertical-align: bottom; border-bottom: 1.5px solid #9ca3af; border-left: 1px solid #e5e7eb; }
6349
+ th.span { text-align: center; }
6350
+ th:first-child { border-left: 0; }
6351
+ thead { display: table-header-group; }
6352
+ tr { break-inside: avoid; page-break-inside: avoid; }
6353
+ .num { text-align: right; color: #9ca3af; }
6354
+ tr.group td { background: #eef2f7; font-weight: 600; border-top: 1px solid #cbd5e1; break-after: avoid; page-break-after: avoid; }
6355
+ tr.group.level-0 td { background: #e2e8f0; }
6356
+ tr.title td { border: 0; padding: 0 0 4px; background: none; }
6357
+ tr.total td { font-weight: 700; background: #f3f4f6; border-top: 1.5px solid #111827; border-bottom: 0; }
6358
+ `;
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 = {}) {
5091
6361
  const cols = exportColumns(grid, opts.columns);
5092
- const code = grid.store.state.locale.code;
6362
+ const loc = grid.store.state.locale;
6363
+ const code = loc.code;
5093
6364
  const fmts = cols.map((c) => getFormatter(c.def, code));
6365
+ const nf = new Intl.NumberFormat(code);
6366
+ const numbered = opts.rowNumbers ?? !!grid.options.rowNumbers;
6367
+ const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
5094
6368
  const chains = cols.map((c) => {
5095
6369
  const chain = [];
5096
6370
  for (let n = c.node; n; n = n.parent) chain.unshift(n);
@@ -5098,41 +6372,192 @@ function buildPrintHtml(grid, opts = {}) {
5098
6372
  });
5099
6373
  const depth = Math.max(1, ...chains.map((ch) => ch.length));
5100
6374
  const head = [];
5101
- for (let level = 0; level < depth; level++) {
5102
- let tr = "";
5103
- for (let ci = 0; ci < cols.length; ci++) {
5104
- const chain = chains[ci];
5105
- const leaf = chain.length - 1;
5106
- if (level < leaf) {
5107
- const g = chain[level];
5108
- if (ci > 0 && chains[ci - 1][level] === g) continue;
5109
- let span = 1;
5110
- while (ci + span < cols.length && chains[ci + span][level] === g) span++;
5111
- tr += `<th colspan="${span}">${escHtml(header(g.def, g.id))}</th>`;
5112
- } else if (level === leaf) {
5113
- tr += `<th colspan="1" rowspan="${depth - level}">${escHtml(header(cols[ci].def, cols[ci].id))}</th>`;
5114
- }
5115
- }
5116
- 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
+ }
5117
6393
  }
5118
- const align = (def) => ALIGNS.includes(def.align) ? def.align : def.type === "number" ? "right" : def.type === "boolean" ? "center" : "left";
5119
- const body = exportRows(grid, opts.rows).map((r) => `<tr>${cols.map((c, i) => {
5120
- const v = getCellValue(c.def, r);
5121
- return `<td style="text-align:${align(c.def)}">${escHtml(isBlank(v) ? "" : fmts[i](v))}</td>`;
5122
- }).join("")}</tr>`).join("");
5123
- const title = escHtml(opts.title ?? document.title ?? "");
5124
- return `<!doctype html><html lang="${escHtml(code)}"><head><meta charset="utf-8"><title>${title}</title><style>
5125
- @page { size: auto; margin: 12mm; }
5126
- body { font: 11px/1.4 system-ui, 'Pretendard', 'Malgun Gothic', sans-serif; color: #111; margin: 0; }
5127
- h1 { font-size: 15px; margin: 0 0 8px; }
5128
- table { border-collapse: collapse; width: 100%; }
5129
- th, td { border: 1px solid #999; padding: 3px 6px; }
5130
- th { background: #eee; font-weight: 600; }
5131
- thead { display: table-header-group; }
5132
- tr { page-break-inside: avoid; }
5133
- </style></head><body>${opts.title ? `<h1>${title}</h1>` : ""}<table><thead>${opts.headers === false ? "" : head.join("")}</thead><tbody>${body}</tbody></table></body></html>`;
5134
- }
5135
- function printHtml(html) {
6394
+ const aggText = (c, i, v, agg) => {
6395
+ if (v == null || v === "") return "";
6396
+ if (agg === "count") return nf.format(v);
6397
+ const type = c.def.type ?? (typeof v === "number" ? "number" : v instanceof Date ? "date" : void 0);
6398
+ return type === c.def.type ? fmts[i](v) : getFormatter({ ...c.def, type }, code)(v);
6399
+ };
6400
+ const labelled = (kind, label, values, level = 0) => {
6401
+ let lead = 0;
6402
+ while (lead < cols.length - 1 && !values[lead]) lead++;
6403
+ lead = Math.max(1, lead);
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 };
6408
+ };
6409
+ const rows = exportRows(grid, opts.rows);
6410
+ const groupBy = opts.groups !== false && !grid.rowModel.server && !grid.options.treeData ? grid.getGroupBy() : [];
6411
+ const probe = opts.styles !== false && cols.some((c) => c.def.cellClass) ? classStyles(grid) : null;
6412
+ const body = [];
6413
+ try {
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)})`;
6419
+ const values = cols.map((c, i) => {
6420
+ if (!c.def.aggFunc || groupBy.includes(c.id)) return void 0;
6421
+ try {
6422
+ return aggText(c, i, aggregate(c.def.aggFunc, line2.rows.map((r2) => getCellValue(c.def, r2)), line2.rows), c.def.aggFunc) || void 0;
6423
+ } catch {
6424
+ return void 0;
6425
+ }
6426
+ });
6427
+ body.push(labelled("group", label, values, line2.level));
6428
+ continue;
6429
+ }
6430
+ const r = line2.row;
6431
+ const cells = numbered ? [{ text: String(line2.n), align: "right", num: true }] : [];
6432
+ cols.forEach((c, i) => {
6433
+ const v = getCellValue(c.def, r);
6434
+ const text2 = isBlank(v) ? "" : fmts[i](v);
6435
+ const cell = { text: text2, align: align(c.def) };
6436
+ if (probe && c.def.cellClass) {
6437
+ let cls;
6438
+ try {
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;
6440
+ } catch {
6441
+ cls = void 0;
6442
+ }
6443
+ if (cls) {
6444
+ const st = probe.of(cls);
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;
6448
+ }
6449
+ }
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);
6462
+ });
6463
+ body.push({ kind: "row", cells });
6464
+ }
6465
+ } finally {
6466
+ probe?.dispose();
6467
+ }
6468
+ const foot = [];
6469
+ const summary = opts.summary !== false ? grid.summary() : null;
6470
+ if (summary) {
6471
+ summary.opts.rows.forEach((def, ri) => {
6472
+ const values = cols.map((c, i) => {
6473
+ const spec = def.columns?.[c.id];
6474
+ return spec === void 0 ? void 0 : aggText(c, i, summary.values[ri]?.[c.id], cellSpec(spec).agg) || void 0;
6475
+ });
6476
+ foot.push(labelled("total", def.label ?? "", values));
6477
+ });
6478
+ }
6479
+ const widths = cols.map((c) => Math.max(1, c.width));
6480
+ const sum = widths.reduce((a, b) => a + b, 0);
6481
+ const paper = PAPERS.includes(opts.paper) ? opts.paper : "A4";
6482
+ const orientation = opts.orientation === "portrait" || opts.orientation === "landscape" ? opts.orientation : sum + (numbered ? 40 : 0) > PORTRAIT_WIDTH ? "landscape" : "portrait";
6483
+ const printed = opts.printedAt !== false ? new Intl.DateTimeFormat(code, { dateStyle: "medium", timeStyle: "short" }).format(/* @__PURE__ */ new Date()) : "";
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>` : "";
6542
+ const small = "font: 8.5px system-ui, sans-serif; color: #6b7280;";
6543
+ const page = [
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} }` : ""
6547
+ ].join(" ");
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>
6557
+ @page { ${page} }${PRINT_CSS}${stripes}
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>`;
6559
+ }
6560
+ function printHtml(html) {
5136
6561
  if (typeof document === "undefined") return;
5137
6562
  const frame = document.createElement("iframe");
5138
6563
  frame.style.cssText = "position:fixed;right:0;bottom:0;width:0;height:0;border:0";
@@ -5172,6 +6597,22 @@ function buildCsv(grid, opts = {}) {
5172
6597
  if (opts.headers !== false) matrix.unshift(cols.map((c) => ({ value: header(c.def, c.id), text: true })));
5173
6598
  return toCsv(matrix, sep);
5174
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
+ }
5175
6616
  function excelFormat(def) {
5176
6617
  const f = def.format;
5177
6618
  if (def.type === "date") {
@@ -5194,9 +6635,9 @@ function excelFormat(def) {
5194
6635
  return `${o.useGrouping === false ? "0" : "#,##0"}${decimals}`;
5195
6636
  }
5196
6637
  function toArgb(css) {
5197
- const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(css.trim());
5198
- if (hex) {
5199
- 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];
5200
6641
  return `FF${h.toUpperCase()}`;
5201
6642
  }
5202
6643
  const m = /^rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)(?:[\s,/]+([\d.]+%?))?\s*\)$/i.exec(css.trim());
@@ -5295,7 +6736,7 @@ function buildXlsxSheet(grid, opts = {}) {
5295
6736
  return Number.isFinite(n) ? { v: n, fmt } : String(v);
5296
6737
  }
5297
6738
  case "date": {
5298
- const d = toDate(v);
6739
+ const d = toDate2(v);
5299
6740
  return d ? { v: d, fmt } : String(v);
5300
6741
  }
5301
6742
  case "boolean":
@@ -5320,6 +6761,13 @@ function buildXlsxSheet(grid, opts = {}) {
5320
6761
  }
5321
6762
  if (cls) style = { ...style, ...probe.of(cls) };
5322
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
+ }
5323
6771
  if (!Object.keys(style).length) return cell;
5324
6772
  return cell !== null && typeof cell === "object" && !(cell instanceof Date) ? { ...cell, ...style } : { v: cell, ...style };
5325
6773
  }));
@@ -5353,6 +6801,8 @@ function buildXlsxSheet(grid, opts = {}) {
5353
6801
  widths: cols.map((c) => Math.round(c.width / 7)),
5354
6802
  merges,
5355
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),
5356
6806
  autoFilter: headRows > 0
5357
6807
  };
5358
6808
  }
@@ -5421,13 +6871,13 @@ async function importInto(grid, file, opts = {}) {
5421
6871
  const sample = grid.store.state.data[0];
5422
6872
  let skipped = 0;
5423
6873
  const rows = [];
5424
- for (const line of matrix.slice(headRows)) {
5425
- 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;
5426
6876
  const row = {};
5427
6877
  mapped.forEach((node, i) => {
5428
6878
  if (!node) return;
5429
6879
  const def = node.def;
5430
- const raw = asText(line[i]);
6880
+ const raw = asText(line2[i]);
5431
6881
  const res = grid.acceptText(def, row, raw, sample?.[def.field]);
5432
6882
  if (res.ok) {
5433
6883
  if (!(raw === "" && (res.value === null || res.value === ""))) row[def.field] = res.value;
@@ -5778,58 +7228,6 @@ var GridCharts = class {
5778
7228
  }
5779
7229
  };
5780
7230
 
5781
- // src/rows/sort.ts
5782
- function compareNonBlank(a, b, collator) {
5783
- if (typeof a === "number" && typeof b === "number") return a - b;
5784
- if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
5785
- if (typeof a === "boolean" && typeof b === "boolean") return Number(a) - Number(b);
5786
- return collator.compare(String(a), String(b));
5787
- }
5788
- function normalize(def, v) {
5789
- if (isBlank(v)) return null;
5790
- if (def.type === "number") {
5791
- const n = typeof v === "number" ? v : Number(v);
5792
- return Number.isNaN(n) ? null : n;
5793
- }
5794
- if (def.type === "date") return toDate(v)?.getTime() ?? null;
5795
- return v;
5796
- }
5797
- function compareValues(a, b, collator) {
5798
- const ba = isBlank(a), bb = isBlank(b);
5799
- if (ba || bb) return ba === bb ? 0 : ba ? 1 : -1;
5800
- return compareNonBlank(a, b, collator);
5801
- }
5802
- function sortNodes(nodes, sort, resolve, collator) {
5803
- const keys = sort.map((s) => ({ def: resolve(s.colId), mul: s.dir === "desc" ? -1 : 1 })).filter((k) => !!k.def);
5804
- if (!keys.length) return nodes;
5805
- const values = keys.map((k) => nodes.map((n) => {
5806
- const v = getCellValue(k.def, n.data);
5807
- return k.def.comparator ? v : normalize(k.def, v);
5808
- }));
5809
- const order = nodes.map((_, i) => i);
5810
- order.sort((i, j) => {
5811
- for (let k = 0; k < keys.length; k++) {
5812
- const { def, mul } = keys[k];
5813
- const a = values[k][i], b = values[k][j];
5814
- let c;
5815
- if (def.comparator) {
5816
- try {
5817
- c = (Number(def.comparator(a, b, nodes[i].data, nodes[j].data)) || 0) * mul;
5818
- } catch {
5819
- warn(`comparator of column "${def.id ?? def.field}" threw; treating values as equal`);
5820
- c = 0;
5821
- }
5822
- } else {
5823
- const ba = isBlank(a), bb = isBlank(b);
5824
- c = ba || bb ? ba === bb ? 0 : ba ? 1 : -1 : compareNonBlank(a, b, collator) * mul;
5825
- }
5826
- if (c) return c;
5827
- }
5828
- return i - j;
5829
- });
5830
- return order.map((i) => nodes[i]);
5831
- }
5832
-
5833
7231
  // src/pivot.ts
5834
7232
  var SOURCE = /* @__PURE__ */ new WeakMap();
5835
7233
  var aggName = (a) => typeof a === "function" ? "custom" : a;
@@ -5955,7 +7353,7 @@ function buildPivot(source, config, defOf, labels, locale) {
5955
7353
  }
5956
7354
 
5957
7355
  // src/version.ts
5958
- var version = "1.0.1";
7356
+ var version = "1.6.0";
5959
7357
 
5960
7358
  // src/license.ts
5961
7359
  var PUBLIC_KEY = { kty: "EC", crv: "P-256", x: "sUTKkglZmk-HMKPNub8p7AeLBKesHCU8irIkw9rJlbE", y: "j0U_aKRDR7iApY9UpaD3hUemhBLZJwDv9-QiT_hUyw8" };
@@ -6143,6 +7541,7 @@ var ColumnMenu = class {
6143
7541
  ];
6144
7542
  if (!def.lockVisible) items.push([loc.hideColumn, () => this.grid.setColumnsVisible([colId], false)]);
6145
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)]);
6146
7545
  popup.append(this.section(items));
6147
7546
  const filter = this.filterSection(colId, () => this.close());
6148
7547
  if (filter) popup.append(filter);
@@ -6189,11 +7588,11 @@ var ColumnMenu = class {
6189
7588
  }
6190
7589
  section(items) {
6191
7590
  const s = el("div", "fg-menu-section");
6192
- for (const [label, run] of items) {
7591
+ for (const [label, run2] of items) {
6193
7592
  const b = el("button", "fg-menu-item", { type: "button" });
6194
7593
  b.textContent = label;
6195
7594
  b.addEventListener("click", () => {
6196
- run();
7595
+ run2();
6197
7596
  this.close();
6198
7597
  });
6199
7598
  s.append(b);
@@ -7325,6 +8724,611 @@ var FilterBuilder = class {
7325
8724
  }
7326
8725
  };
7327
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
+
7328
9332
  // src/grid.ts
7329
9333
  var autoRowHeight = (size) => Math.round(size * 2.75);
7330
9334
  var autoHeaderHeight = (size) => Math.round(size * 3.1);
@@ -7369,11 +9373,34 @@ var Grid = class {
7369
9373
  /** 0-based page and rows per page (pagination only). */
7370
9374
  this.page = 0;
7371
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);
7372
9398
  // ── pagination and server rows ──────────────────────────────
7373
9399
  /** Cells flashing after a transaction: `${rowId}\u0000${colId}` → time the flash ends. */
7374
9400
  /** showOverlay: an overlay the app asked for, shown whatever the row count. */
7375
9401
  this.forcedOverlay = null;
7376
9402
  this.filterBuilder = new FilterBuilder(this);
9403
+ /** `${rowId}\u0000${colId}` → when the flash ends and which way a number moved. */
7377
9404
  this.flashing = /* @__PURE__ */ new Map();
7378
9405
  this.flashTimer = 0;
7379
9406
  this.txQueue = [];
@@ -7413,6 +9440,7 @@ var Grid = class {
7413
9440
  });
7414
9441
  this.sourceDefs = Array.isArray(options.columns) ? options.columns : [];
7415
9442
  this.columns = new ColumnModel(this.withSelect(this.sourceDefs));
9443
+ this.bindFormulas();
7416
9444
  const pg = options.pagination;
7417
9445
  if (pg && typeof pg === "object" && pg.pageSize !== void 0) {
7418
9446
  if (Number.isInteger(pg.pageSize) && pg.pageSize > 0) this.pageSize = pg.pageSize;
@@ -7434,6 +9462,7 @@ var Grid = class {
7434
9462
  collator: () => this.collator()
7435
9463
  });
7436
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] }));
7437
9466
  this.defaultExpanded = options.groupDefaultExpanded ?? 0;
7438
9467
  if (options.treeData && !options.getRowId) warn("treeData needs getRowId so parent ids can be matched");
7439
9468
  this.updateRows();
@@ -7449,6 +9478,7 @@ var Grid = class {
7449
9478
  });
7450
9479
  this.density = this.densityOf(options.density);
7451
9480
  this.renderer.applyTheme();
9481
+ if (options.flashDuration) this.renderer.root.style.setProperty("--fg-flash-duration", `${this.flashMs()}ms`);
7452
9482
  this.renderer.applyFont(this.font.size, this.font.family);
7453
9483
  this.menu = new ColumnMenu(this);
7454
9484
  this.chooser = new ColumnChooserDialog(this);
@@ -7475,6 +9505,8 @@ var Grid = class {
7475
9505
  () => this.groupPanel?.destroy(),
7476
9506
  () => this.charts.destroy(),
7477
9507
  () => this.filterBuilder.close(),
9508
+ () => this.formatDialog.close(),
9509
+ () => this.assistantPanel.close(),
7478
9510
  attachTooltips(this)
7479
9511
  );
7480
9512
  if (typeof ResizeObserver !== "undefined") {
@@ -7490,6 +9522,45 @@ var Grid = class {
7490
9522
  this.api = this.createApi();
7491
9523
  liveGrids.set(this.renderer.root, this);
7492
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
+ }
7493
9564
  collator() {
7494
9565
  const code = this.store.state.locale.code;
7495
9566
  if (this.collatorCache?.code !== code) {
@@ -7627,17 +9698,35 @@ var Grid = class {
7627
9698
  isFlashing(rowId, colId) {
7628
9699
  return this.flashing.size > 0 && this.flashing.has(`${rowId}\0${colId}`);
7629
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
+ }
7630
9711
  flash(keys) {
7631
- const until = Date.now() + 1e3;
7632
- 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 });
7633
9715
  if (this.flashTimer) return;
7634
9716
  const tick = () => {
7635
9717
  const now = Date.now();
7636
- for (const [k, t] of this.flashing) if (t <= now) this.flashing.delete(k);
7637
- this.renderer.invalidate({ rows: true });
7638
- 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;
7639
9728
  };
7640
- this.flashTimer = window.setTimeout(tick, 1e3);
9729
+ this.flashTimer = window.setTimeout(tick, ms);
7641
9730
  }
7642
9731
  applyTransaction(tx) {
7643
9732
  if (!tx || typeof tx !== "object") return { added: [], updated: [], removed: [], notFound: [] };
@@ -7652,32 +9741,55 @@ var Grid = class {
7652
9741
  const f = this.columns.getNode(st.colId)?.def.field;
7653
9742
  if (f) colsByField.set(f, [...colsByField.get(f) ?? [], st.colId]);
7654
9743
  }
7655
- const flashKeys = [];
9744
+ const flashKeys = /* @__PURE__ */ new Map();
9745
+ const changed = { fields: /* @__PURE__ */ new Set(), ids: /* @__PURE__ */ new Set() };
7656
9746
  let next = this.store.state.data;
7657
9747
  const present = new Set(next);
7658
9748
  const byId = new Map(next.map((r) => [this.rowModel.idOf(r), r]));
7659
9749
  for (const tx of list) {
7660
- 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);
7661
9751
  }
7662
9752
  if (!result.added.length && !result.updated.length && !result.removed.length) {
7663
9753
  if (result.notFound.length) this.events.emit("transactionApplied", result);
7664
9754
  return result;
7665
9755
  }
7666
9756
  this.editVersion++;
7667
- this.renderer.invalidateHeights();
7668
- this.keepRange = true;
7669
- try {
7670
- this.replaceData(next === this.store.state.data ? [...next] : next);
7671
- } finally {
7672
- 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
+ }
7673
9772
  }
7674
9773
  this.editor.sync();
7675
- if (flashKeys.length && this.options.flashCells !== false) this.flash(flashKeys);
9774
+ if (flashKeys.size && this.options.flashCells !== false) this.flash(flashKeys);
7676
9775
  this.events.emit("transactionApplied", result);
7677
9776
  return result;
7678
9777
  }
7679
9778
  /** One transaction against `data` (remove → update → add); returns the new array. */
7680
- 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) {
7681
9793
  const getId = this.options.getRowId;
7682
9794
  const { present, byId } = index;
7683
9795
  const keyOf = (r) => {
@@ -7702,6 +9814,20 @@ var Grid = class {
7702
9814
  this.tracker.originals.delete(row);
7703
9815
  } else if (!row) result.notFound.push(r);
7704
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
+ };
7705
9831
  for (const r of tx.update ?? []) {
7706
9832
  const k = keyOf(r);
7707
9833
  const row = k === null ? void 0 : byId.get(k);
@@ -7709,16 +9835,16 @@ var Grid = class {
7709
9835
  result.notFound.push(r);
7710
9836
  continue;
7711
9837
  }
7712
- const orig = this.tracker.originals.get(row);
7713
- for (const [field, value] of Object.entries(r)) {
7714
- if (orig) orig[field] = value;
7715
- if (Object.is(row[field], value)) continue;
7716
- row[field] = value;
7717
- for (const colId of colsByField.get(field) ?? []) flashKeys.push(`${k}\0${colId}`);
7718
- }
7719
- result.updated.push(row);
9838
+ update(r, k, row);
7720
9839
  }
7721
- 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);
7722
9848
  result.added.push(...added);
7723
9849
  for (const r of added) {
7724
9850
  present.add(r);
@@ -7734,13 +9860,16 @@ var Grid = class {
7734
9860
  if (!tx || typeof tx !== "object") return;
7735
9861
  this.txQueue.push(tx);
7736
9862
  if (this.txTimer) return;
7737
- this.txTimer = window.setTimeout(() => {
7738
- this.txTimer = 0;
7739
- const queue = this.txQueue;
7740
- this.txQueue = [];
7741
- if (this.destroyed || !queue.length) return;
7742
- this.applyTransactions(queue);
7743
- }, 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);
7744
9873
  }
7745
9874
  setAdvancedFilter(filter) {
7746
9875
  const next = filter && typeof filter === "object" && Array.isArray(filter.conditions) && filter.conditions.length ? filter : null;
@@ -7769,6 +9898,7 @@ var Grid = class {
7769
9898
  expanded: Object.fromEntries(expanded),
7770
9899
  selected: [...s.selected]
7771
9900
  };
9901
+ if (this.cfRules.length) state.conditionalFormats = this.getConditionalFormats();
7772
9902
  if (this.options.pagination) {
7773
9903
  state.page = this.page;
7774
9904
  state.pageSize = this.pageSize;
@@ -7806,6 +9936,7 @@ var Grid = class {
7806
9936
  }
7807
9937
  if (want("pageSize") && typeof state.pageSize === "number") this.setPageSize(state.pageSize);
7808
9938
  if (want("page") && typeof state.page === "number") this.setPage(state.page);
9939
+ if (want("conditionalFormats") && Array.isArray(state.conditionalFormats)) this.setConditionalFormats(state.conditionalFormats);
7809
9940
  if (want("selected") && Array.isArray(state.selected)) {
7810
9941
  const ids = new Set(this.rowModel.allNodes().map((n) => n.id));
7811
9942
  this.setSelected(new Set(state.selected.map(String).filter((id) => ids.has(id))));
@@ -9055,6 +11186,7 @@ var Grid = class {
9055
11186
  return;
9056
11187
  }
9057
11188
  this.columns.setDefs(this.withSelect(defs));
11189
+ this.bindFormulas();
9058
11190
  const known = (id) => !!this.columns.getNode(id);
9059
11191
  const groupBy = this.groupBy.filter(known);
9060
11192
  if (groupBy.length !== this.groupBy.length) {
@@ -9132,6 +11264,12 @@ var Grid = class {
9132
11264
  getXlsxSheet: guard((o) => buildXlsxSheet(this, o)),
9133
11265
  print: guard((o) => printHtml(buildPrintHtml(this, o))),
9134
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
+ }),
9135
11273
  setColumnGroupOpened: guard((id, open) => this.setColumnGroupOpened(id, open)),
9136
11274
  setAdvancedFilter: guard((f) => this.setAdvancedFilter(f)),
9137
11275
  getAdvancedFilter: guard(() => this.store.state.advancedFilter ? JSON.parse(JSON.stringify(this.store.state.advancedFilter)) : null),
@@ -9139,6 +11277,12 @@ var Grid = class {
9139
11277
  isColumnGroupOpened: guard((id) => !!this.columns.getNode(id)?.open),
9140
11278
  applyTransaction: guard((tx) => this.applyTransaction(tx)),
9141
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()),
9142
11286
  getState: guard(() => this.getState()),
9143
11287
  setState: guard((s, o) => this.setState(s, o)),
9144
11288
  getScrollPosition: guard(() => this.renderer.getScroll()),
@@ -9205,6 +11349,8 @@ var Grid = class {
9205
11349
  getPivot: guard(() => this.getPivot()),
9206
11350
  createChart: guard((o) => this.charts.create(o)),
9207
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")),
9208
11354
  exportCsv: guard((name = "export.csv", o) => download(name, ["\uFEFF", buildCsv(this, o)], "text/csv;charset=utf-8")),
9209
11355
  getXlsx: guard((o) => buildXlsx(this, o)),
9210
11356
  exportXlsx: guard((name = "export.xlsx", o) => download(name, [buildXlsx(this, o)], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")),
@@ -9302,13 +11448,13 @@ function saveChanges(grid, url, options = {}) {
9302
11448
  if (!grid.hasChanges()) return Promise.resolve(null);
9303
11449
  const { created, updated, deleted } = grid.getModifiedRows();
9304
11450
  const rows = new Map([...created, ...updated].map((r) => [r, { ...r }]));
9305
- const run = (async () => {
11451
+ const run2 = (async () => {
9306
11452
  const json = await send(url, "POST", { created, updated, deleted, ...options.params?.() ?? {} }, options);
9307
11453
  grid.commitChanges({ rows, deleted });
9308
11454
  return json;
9309
11455
  })().finally(() => saving.delete(grid));
9310
- saving.set(grid, run);
9311
- return run;
11456
+ saving.set(grid, run2);
11457
+ return run2;
9312
11458
  }
9313
11459
  async function loadData(grid, url, options = {}) {
9314
11460
  const { rows } = await restDatasource(url, options).getRows({ start: 0, end: options.maxRows ?? 1e4, sort: [], filters: {}, quickFilter: "" });
@@ -9320,15 +11466,186 @@ function csrfHeaders(doc = document) {
9320
11466
  const header2 = doc.querySelector('meta[name="_csrf_header"]')?.content || "X-CSRF-TOKEN";
9321
11467
  return token ? { [header2]: token } : {};
9322
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
+ }
9323
11635
  export {
9324
11636
  buildWorkbook,
11637
+ connectLive,
9325
11638
  createGrid,
9326
11639
  csrfHeaders,
9327
11640
  en_default as en,
11641
+ evaluateFormula,
9328
11642
  exportWorkbook,
11643
+ formulaFunctions,
9329
11644
  loadData,
11645
+ messageToTransaction,
9330
11646
  parseCsv,
9331
11647
  readXlsx,
11648
+ registerFormulaFunction,
9332
11649
  registerLocale,
9333
11650
  renderChart,
9334
11651
  resolveLocale,