@svgrid/grid 3.0.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/GridFooter.svelte +14 -5
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/GridMenus.svelte +18 -10
  4. package/dist/GridMenus.svelte.d.ts +2 -1
  5. package/dist/SvGrid.controller.svelte.d.ts +17 -0
  6. package/dist/SvGrid.controller.svelte.js +128 -14
  7. package/dist/SvGrid.css +79 -10
  8. package/dist/SvGrid.svelte +191 -186
  9. package/dist/SvGrid.types.d.ts +42 -1
  10. package/dist/SvGridChart.svelte +571 -41
  11. package/dist/SvGridChart.svelte.d.ts +63 -1
  12. package/dist/SvGridChartPanel.svelte +169 -21
  13. package/dist/SvGridChartView.svelte +9 -0
  14. package/dist/SvGridChartView.svelte.d.ts +7 -0
  15. package/dist/SvGroupCell.svelte +28 -3
  16. package/dist/SvGroupCell.svelte.d.ts +8 -0
  17. package/dist/SvRowGroupPanel.svelte +20 -4
  18. package/dist/SvRowGroupPanel.svelte.d.ts +8 -0
  19. package/dist/ai.d.ts +11 -2
  20. package/dist/ai.js +51 -7
  21. package/dist/cdn/GridMenus-DyFA-QxA.js +602 -0
  22. package/dist/cdn/{GridMenus-DFB6iwF8.js → GridMenus-M2XP9t0_.js} +152 -150
  23. package/dist/cdn/SvGridChart-DAb9RbRs.js +1480 -0
  24. package/dist/cdn/SvGridChart-Yl-IXZFO.js +1481 -0
  25. package/dist/cdn/SvGridChartPanel-DayQpDsR.js +834 -0
  26. package/dist/cdn/SvGridChartPanel-aqf8n-vJ.js +810 -0
  27. package/dist/cdn/{SvGridChartView-BhUEJ5ki.js → SvGridChartView-CxfakxR0.js} +5 -3
  28. package/dist/cdn/{SvGridChartView-00LPUHL1.js → SvGridChartView-Dm_YEx9L.js} +5 -3
  29. package/dist/cdn/cell-formatting-C2Hf5gqd.js +86 -0
  30. package/dist/cdn/chart-CKJRCPOk.js +1652 -0
  31. package/dist/cdn/{export-format-Cv1Dll6k.js → export-format-CUDLy2yn.js} +84 -168
  32. package/dist/cdn/{src-C8QjSaq-.js → src-BJ2opd0j.js} +3608 -3414
  33. package/dist/cdn/{src-D0hO2eYN.js → src-DNbz94Ct.js} +8617 -8423
  34. package/dist/cdn/svgrid.js +16 -15
  35. package/dist/cdn/svgrid.svelte-external.js +16 -15
  36. package/dist/cell-formatting.d.ts +7 -0
  37. package/dist/cell-formatting.js +7 -1
  38. package/dist/chart-export.d.ts +23 -2
  39. package/dist/chart-export.js +80 -6
  40. package/dist/chart.d.ts +410 -18
  41. package/dist/chart.js +824 -24
  42. package/dist/grid-icons.d.ts +49 -0
  43. package/dist/grid-icons.js +86 -0
  44. package/dist/index.d.ts +3 -2
  45. package/dist/index.js +3 -2
  46. package/package.json +11 -11
  47. package/src/GridFooter.svelte +14 -5
  48. package/src/GridMenus.svelte +18 -10
  49. package/src/SvGrid.controller.svelte.ts +146 -13
  50. package/src/SvGrid.css +79 -10
  51. package/src/SvGrid.svelte +191 -186
  52. package/src/SvGrid.types.ts +42 -1
  53. package/src/SvGridChart.svelte +571 -41
  54. package/src/SvGridChart.test.ts +243 -1
  55. package/src/SvGridChartPanel.svelte +169 -21
  56. package/src/SvGridChartView.svelte +9 -0
  57. package/src/SvGroupCell.svelte +28 -3
  58. package/src/SvRowGroupPanel.svelte +20 -4
  59. package/src/ai.test.ts +60 -1
  60. package/src/ai.ts +59 -6
  61. package/src/cell-formatting.ts +7 -1
  62. package/src/chart-export.test.ts +112 -0
  63. package/src/chart-export.ts +76 -8
  64. package/src/chart.test.ts +830 -1
  65. package/src/chart.ts +1053 -32
  66. package/src/grid-icons.test.ts +46 -0
  67. package/src/grid-icons.ts +184 -0
  68. package/src/icon-overrides.grid.test.ts +188 -0
  69. package/src/icon-seam.test.ts +100 -0
  70. package/src/index.ts +19 -0
  71. package/src/svgrid.charting.test.ts +120 -0
  72. package/dist/cdn/GridMenus-CYzOvmNQ.js +0 -600
  73. package/dist/cdn/SvGridChart-BEJmNNx9.js +0 -1203
  74. package/dist/cdn/SvGridChart-BwVos976.js +0 -1202
  75. package/dist/cdn/SvGridChartPanel-BOknOkrP.js +0 -561
  76. package/dist/cdn/SvGridChartPanel-D2PjII4O.js +0 -585
  77. package/dist/cdn/chart-i4XcXyHJ.js +0 -1214
package/src/SvGrid.svelte CHANGED
@@ -23,6 +23,7 @@
23
23
  } from "./render-component";
24
24
  import { buildSparkline, toSparklineValues } from "./sparkline";
25
25
  import { localizeOperatorLabel } from "./filter-operators";
26
+ import { GRID_ICON_GLYPHS, type GridIconName } from "./grid-icons";
26
27
  // The whole editor UI - including the lazy SvGridDropdown / SvDateTimePicker
27
28
  // and the custom-editor registry - now lives in SvGridCellEditor.svelte, which
28
29
  // this component loads with import() once editing is enabled.
@@ -671,9 +672,19 @@
671
672
  for (const chip of chips.slice(fit)) chip.style.display = "none";
672
673
  const hidden = chips.length - fit;
673
674
  more.textContent = `+${hidden}`;
675
+ // Read the label element rather than stripping the remove button's glyph
676
+ // off the end of the chip's text. That regex assumed the button always
677
+ // renders one specific character, which stopped being true once `icons`
678
+ // let a consumer replace it - any other glyph, or an SVG, leaked into
679
+ // this tooltip.
674
680
  more.title = chips
675
681
  .slice(fit)
676
- .map((c) => c.textContent?.replace(/×$/, "").trim() ?? "")
682
+ .map(
683
+ (c) =>
684
+ (
685
+ c.querySelector(".sv-grid-filter-chip-label") ?? c
686
+ ).textContent?.trim() ?? "",
687
+ )
677
688
  .join(", ");
678
689
  };
679
690
  // Measure after layout settles.
@@ -734,6 +745,158 @@
734
745
  </p>
735
746
  {/snippet}
736
747
 
748
+ <!-- Every icon the grid draws for its own chrome comes through here, so the
749
+ `icons` prop has exactly one place to intercept. Three branches, in
750
+ order: a consumer override, a glyph icon whose default is a character,
751
+ and the built-in path set.
752
+
753
+ The override is wrapped in `.sv-grid-icon` rather than the consumer
754
+ being asked to add that class themselves. Sizing and the expander's
755
+ rotate-on-open both hang off it (SvGrid.css), and an override that
756
+ dropped the class would lose both silently - which is the kind of bug
757
+ that only shows up in someone else's app. -->
758
+ {#snippet icon(name: GridIconName)}
759
+ {@const override = opt.icons?.[name]}
760
+ {#if override}
761
+ <span class="sv-grid-icon sv-grid-icon-custom" aria-hidden="true"
762
+ >{@render override()}</span
763
+ >
764
+ {:else if GRID_ICON_GLYPHS[name]}
765
+ {GRID_ICON_GLYPHS[name]}
766
+ {:else}
767
+ <svg
768
+ class="sv-grid-icon"
769
+ viewBox="0 0 24 24"
770
+ fill="none"
771
+ stroke="currentColor"
772
+ stroke-width="2.2"
773
+ stroke-linecap="round"
774
+ stroke-linejoin="round"
775
+ aria-hidden="true"
776
+ >
777
+ {#if name === "sort"}
778
+ <path d="M8 10l4-4 4 4" />
779
+ <path d="M8 14l4 4 4-4" />
780
+ {:else if name === "sort-asc"}
781
+ <path d="M6 14l6-6 6 6" />
782
+ {:else if name === "sort-desc"}
783
+ <path d="M6 10l6 6 6-6" />
784
+ {:else if name === "filter"}
785
+ <path d="M3 5h18l-7 8v6l-4 2v-8z" />
786
+ {:else if name === "menu"}
787
+ <path d="M4 7h16" />
788
+ <path d="M4 12h16" />
789
+ <path d="M4 17h16" />
790
+ {:else if name === "group"}
791
+ <path d="M12 3l8 4.5-8 4.5-8-4.5z" />
792
+ <path d="M4 12l8 4.5 8-4.5" />
793
+ <path d="M4 16.5l8 4.5 8-4.5" />
794
+ {:else if name === "x" || name === "unpin"}
795
+ <path d="M18 6L6 18" />
796
+ <path d="M6 6l12 12" />
797
+ {:else if name === "chevron-down" || name === "column-group-caret"}
798
+ <path d="M6 9l6 6 6-6" />
799
+ {:else if name === "chevron-right"}
800
+ <!-- Same path SvTree's twisty uses, so every expander in the library
801
+ draws the same chevron. Rotated 90deg by CSS when expanded rather
802
+ than swapped for a second glyph. -->
803
+ <path d="m9 18 6-6-6-6" />
804
+ {:else if name === "op-contains" || name === "search"}
805
+ <!-- The magnifier the operator menu draws is also the one every search
806
+ box in the grid draws. Four hand-inlined copies used to diverge
807
+ here (different viewBox, different stroke width); they share this
808
+ arm now, so re-skinning `search` catches all of them. -->
809
+ <circle cx="11" cy="11" r="6" />
810
+ <path d="M20 20l-4.5-4.5" />
811
+ {:else if name === "op-equals"}
812
+ <path d="M5 9.5h14" />
813
+ <path d="M5 14.5h14" />
814
+ {:else if name === "op-startsWith" || name === "pin-left"}
815
+ <!-- `pin-left` / `pin-right` / `unpin` share the operator glyphs rather
816
+ than borrowing the operator NAMES, which is what the menu used to
817
+ do. Same default, but overriding a filter operator no longer
818
+ silently repaints the pin menu. -->
819
+ <path d="M5 5v14" />
820
+ <path d="M9 9h10" />
821
+ <path d="M9 15h7" />
822
+ {:else if name === "op-greaterThan" || name === "pin-right"}
823
+ <path d="M8 5l9 7-9 7" />
824
+ {:else if name === "op-lessThan"}
825
+ <path d="M16 5l-9 7 9 7" />
826
+ {:else if name === "op-between"}
827
+ <!-- Two bounds with the span between them. The catalogue has always
828
+ named this icon; the snippet had no case for it, so the Between
829
+ row in the operator menu drew an empty <svg>. -->
830
+ <path d="M5 5v14" />
831
+ <path d="M19 5v14" />
832
+ <path d="M9 12h6" />
833
+ {:else if name === "op-isBlank"}
834
+ <circle cx="12" cy="12" r="8" />
835
+ <path d="M6.5 6.5l11 11" />
836
+ {:else if name === "op-isNotBlank"}
837
+ <circle cx="12" cy="12" r="8" />
838
+ <circle cx="12" cy="12" r="3.2" fill="currentColor" stroke="none" />
839
+ {:else if name === "op-notContains"}
840
+ <circle cx="11" cy="11" r="6" />
841
+ <path d="M20 20l-4.5-4.5" />
842
+ <path d="M7 11h8" />
843
+ {:else if name === "op-notEquals"}
844
+ <path d="M5 9.5h14" />
845
+ <path d="M5 14.5h14" />
846
+ <path d="M16 5l-8 14" />
847
+ {:else if name === "op-endsWith"}
848
+ <path d="M19 5v14" />
849
+ <path d="M15 9H5" />
850
+ <path d="M15 15H8" />
851
+ {:else if name === "op-regex"}
852
+ <path d="M12 5v9" />
853
+ <path d="M8.1 7.4l7.8 4.5" />
854
+ <path d="M15.9 7.4l-7.8 4.5" />
855
+ <circle cx="6" cy="18" r="1.4" fill="currentColor" stroke="none" />
856
+ {:else if name === "op-in"}
857
+ <path d="M14 5a7 7 0 1 0 0 14" />
858
+ <path d="M6 12h9" />
859
+ <path d="M12 9l3 3-3 3" />
860
+ {:else if name === "op-notIn"}
861
+ <path d="M14 5a7 7 0 1 0 0 14" />
862
+ <path d="M15 12H6" />
863
+ <path d="M9 9l-3 3 3 3" />
864
+ {:else if name === "autosize"}
865
+ <path d="M3 12h18" />
866
+ <path d="M3 12l4-4" />
867
+ <path d="M3 12l4 4" />
868
+ <path d="M21 12l-4-4" />
869
+ <path d="M21 12l-4 4" />
870
+ {:else if name === "columns"}
871
+ <rect x="3" y="4" width="5" height="16" rx="1" />
872
+ <rect x="10" y="4" width="5" height="16" rx="1" />
873
+ <rect x="17" y="4" width="4" height="16" rx="1" />
874
+ {:else if name === "reset"}
875
+ <path d="M3 4v6h6" />
876
+ <path d="M3.5 10A9 9 0 1 0 6 5.3" />
877
+ {:else if name === "tool-panel"}
878
+ <!-- Deliberately NOT the same rects as `columns`: this one is 6/4/4 and
879
+ `columns` is 5/5/4. Two different marks for two different things,
880
+ and merging them would visibly change the toolbar button. -->
881
+ <rect x="3" y="4" width="6" height="16" rx="1" />
882
+ <rect x="11" y="4" width="4" height="16" rx="1" />
883
+ <rect x="17" y="4" width="4" height="16" rx="1" />
884
+ {:else if name === "chart"}
885
+ <line x1="12" y1="20" x2="12" y2="10" />
886
+ <line x1="18" y1="20" x2="18" y2="4" />
887
+ <line x1="6" y1="20" x2="6" y2="16" />
888
+ {:else if name === "advanced-filter"}
889
+ <!-- A filled-corner funnel, distinct from `filter`'s outline path. It
890
+ marks an ACTIVE advanced filter, so it has to read differently
891
+ from the plain funnel sitting in the header next to it. -->
892
+ <polygon
893
+ points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"
894
+ />
895
+ {/if}
896
+ </svg>
897
+ {/if}
898
+ {/snippet}
899
+
737
900
  {#if opt.loading && !opt.loadingOverlay && !hasMeasured}
738
901
  <!-- Full-screen loading state only on the *initial* load, before the grid
739
902
  has ever rendered. Once measured, a `loading` flip (from a server-mode
@@ -759,10 +922,7 @@
759
922
  >
760
923
  {#if boardConfig.searchable !== false}
761
924
  <label class="sv-grid-board-search">
762
- <svg viewBox="0 0 16 16" aria-hidden="true" width="14" height="14">
763
- <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
764
- <line x1="10.2" y1="10.2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
765
- </svg>
925
+ {@render icon("search")}
766
926
  <input
767
927
  type="search"
768
928
  placeholder={boardConfig.searchPlaceholder ?? "Search cards..."}
@@ -807,10 +967,7 @@
807
967
  >
808
968
  {#if schedulerConfig.searchable !== false}
809
969
  <label class="sv-grid-board-search">
810
- <svg viewBox="0 0 16 16" aria-hidden="true" width="14" height="14">
811
- <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
812
- <line x1="10.2" y1="10.2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
813
- </svg>
970
+ {@render icon("search")}
814
971
  <input
815
972
  type="search"
816
973
  placeholder={schedulerConfig.searchPlaceholder ?? "Search events..."}
@@ -855,10 +1012,7 @@
855
1012
  >
856
1013
  {#if chartViewConfig.searchable !== false}
857
1014
  <label class="sv-grid-board-search">
858
- <svg viewBox="0 0 16 16" aria-hidden="true" width="14" height="14">
859
- <circle cx="7" cy="7" r="4.5" fill="none" stroke="currentColor" stroke-width="1.5" />
860
- <line x1="10.2" y1="10.2" x2="14" y2="14" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
861
- </svg>
1015
+ {@render icon("search")}
862
1016
  <input
863
1017
  type="search"
864
1018
  placeholder={chartViewConfig.searchPlaceholder ?? "Search data..."}
@@ -874,6 +1028,7 @@
874
1028
  data={boardData}
875
1029
  columns={opt.columns}
876
1030
  chart={chartViewConfig}
1031
+ localization={opt.localization}
877
1032
  getRowId={opt.getRowId}
878
1033
  />
879
1034
  {:else if ChartView}
@@ -881,6 +1036,7 @@
881
1036
  data={boardData}
882
1037
  columns={opt.columns}
883
1038
  chart={chartViewConfig}
1039
+ localization={opt.localization}
884
1040
  getRowId={opt.getRowId}
885
1041
  />
886
1042
  {:else}
@@ -927,112 +1083,6 @@
927
1083
  </div>
928
1084
  </div>
929
1085
  {:else}
930
- {#snippet icon(name: string)}
931
- <svg
932
- class="sv-grid-icon"
933
- viewBox="0 0 24 24"
934
- fill="none"
935
- stroke="currentColor"
936
- stroke-width="2.2"
937
- stroke-linecap="round"
938
- stroke-linejoin="round"
939
- aria-hidden="true"
940
- >
941
- {#if name === "sort"}
942
- <path d="M8 10l4-4 4 4" />
943
- <path d="M8 14l4 4 4-4" />
944
- {:else if name === "sort-asc"}
945
- <path d="M6 14l6-6 6 6" />
946
- {:else if name === "sort-desc"}
947
- <path d="M6 10l6 6 6-6" />
948
- {:else if name === "filter"}
949
- <path d="M3 5h18l-7 8v6l-4 2v-8z" />
950
- {:else if name === "menu"}
951
- <path d="M4 7h16" />
952
- <path d="M4 12h16" />
953
- <path d="M4 17h16" />
954
- {:else if name === "group"}
955
- <path d="M12 3l8 4.5-8 4.5-8-4.5z" />
956
- <path d="M4 12l8 4.5 8-4.5" />
957
- <path d="M4 16.5l8 4.5 8-4.5" />
958
- {:else if name === "x"}
959
- <path d="M18 6L6 18" />
960
- <path d="M6 6l12 12" />
961
- {:else if name === "chevron-down"}
962
- <path d="M6 9l6 6 6-6" />
963
- {:else if name === "chevron-right"}
964
- <!-- Same path SvTree's twisty uses, so every expander in the library
965
- draws the same chevron. Rotated 90deg by CSS when expanded rather
966
- than swapped for a second glyph. -->
967
- <path d="m9 18 6-6-6-6" />
968
- {:else if name === "op-contains"}
969
- <circle cx="11" cy="11" r="6" />
970
- <path d="M20 20l-4.5-4.5" />
971
- {:else if name === "op-equals"}
972
- <path d="M5 9.5h14" />
973
- <path d="M5 14.5h14" />
974
- {:else if name === "op-startsWith"}
975
- <path d="M5 5v14" />
976
- <path d="M9 9h10" />
977
- <path d="M9 15h7" />
978
- {:else if name === "op-greaterThan"}
979
- <path d="M8 5l9 7-9 7" />
980
- {:else if name === "op-lessThan"}
981
- <path d="M16 5l-9 7 9 7" />
982
- {:else if name === "op-between"}
983
- <!-- Two bounds with the span between them. The catalogue has always
984
- named this icon; the snippet had no case for it, so the Between
985
- row in the operator menu drew an empty <svg>. -->
986
- <path d="M5 5v14" />
987
- <path d="M19 5v14" />
988
- <path d="M9 12h6" />
989
- {:else if name === "op-isBlank"}
990
- <circle cx="12" cy="12" r="8" />
991
- <path d="M6.5 6.5l11 11" />
992
- {:else if name === "op-isNotBlank"}
993
- <circle cx="12" cy="12" r="8" />
994
- <circle cx="12" cy="12" r="3.2" fill="currentColor" stroke="none" />
995
- {:else if name === "op-notContains"}
996
- <circle cx="11" cy="11" r="6" />
997
- <path d="M20 20l-4.5-4.5" />
998
- <path d="M7 11h8" />
999
- {:else if name === "op-notEquals"}
1000
- <path d="M5 9.5h14" />
1001
- <path d="M5 14.5h14" />
1002
- <path d="M16 5l-8 14" />
1003
- {:else if name === "op-endsWith"}
1004
- <path d="M19 5v14" />
1005
- <path d="M15 9H5" />
1006
- <path d="M15 15H8" />
1007
- {:else if name === "op-regex"}
1008
- <path d="M12 5v9" />
1009
- <path d="M8.1 7.4l7.8 4.5" />
1010
- <path d="M15.9 7.4l-7.8 4.5" />
1011
- <circle cx="6" cy="18" r="1.4" fill="currentColor" stroke="none" />
1012
- {:else if name === "op-in"}
1013
- <path d="M14 5a7 7 0 1 0 0 14" />
1014
- <path d="M6 12h9" />
1015
- <path d="M12 9l3 3-3 3" />
1016
- {:else if name === "op-notIn"}
1017
- <path d="M14 5a7 7 0 1 0 0 14" />
1018
- <path d="M15 12H6" />
1019
- <path d="M9 9l-3 3 3 3" />
1020
- {:else if name === "autosize"}
1021
- <path d="M3 12h18" />
1022
- <path d="M3 12l4-4" />
1023
- <path d="M3 12l4 4" />
1024
- <path d="M21 12l-4-4" />
1025
- <path d="M21 12l-4 4" />
1026
- {:else if name === "columns"}
1027
- <rect x="3" y="4" width="5" height="16" rx="1" />
1028
- <rect x="10" y="4" width="5" height="16" rx="1" />
1029
- <rect x="17" y="4" width="4" height="16" rx="1" />
1030
- {:else if name === "reset"}
1031
- <path d="M3 4v6h6" />
1032
- <path d="M3.5 10A9 9 0 1 0 6 5.3" />
1033
- {/if}
1034
- </svg>
1035
- {/snippet}
1036
1086
 
1037
1087
  {#snippet cellBody(
1038
1088
  row: Row<TData>,
@@ -1370,7 +1420,7 @@
1370
1420
  <td
1371
1421
  class="sv-grid-cell sv-grid-row-number-cell"
1372
1422
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
1373
- >{where === "top" ? "" : ""}</td
1423
+ >{#if where === "top"}{@render icon("pinned-row-top")}{:else}{@render icon("pinned-row-bottom")}{/if}</td
1374
1424
  >
1375
1425
  {/if}
1376
1426
  {#if showRowSelectionEffective}
@@ -1458,9 +1508,7 @@
1458
1508
  to undo it. This is that something: state plus a way out.
1459
1509
  -->
1460
1510
  <span class="sv-grid-advf-chip">
1461
- <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1462
- <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
1463
- </svg>
1511
+ {@render icon("advanced-filter")}
1464
1512
  {messages.advancedFilterActive}
1465
1513
  <button
1466
1514
  type="button"
@@ -1468,7 +1516,7 @@
1468
1516
  aria-label={messages.advancedFilterClear}
1469
1517
  title={messages.advancedFilterClear}
1470
1518
  onclick={() => (ctrl.advancedFilter = null)}
1471
- >✕</button>
1519
+ >{@render icon("clear")}</button>
1472
1520
  </span>
1473
1521
  {/if}
1474
1522
  {#if toolPanelEnabled}
@@ -1482,21 +1530,7 @@
1482
1530
  aria-expanded={ctrl.toolPanelOpen}
1483
1531
  onclick={() => (ctrl.toolPanelOpen = !ctrl.toolPanelOpen)}
1484
1532
  >
1485
- <svg
1486
- viewBox="0 0 24 24"
1487
- width="15"
1488
- height="15"
1489
- fill="none"
1490
- stroke="currentColor"
1491
- stroke-width="2"
1492
- stroke-linecap="round"
1493
- stroke-linejoin="round"
1494
- aria-hidden="true"
1495
- >
1496
- <rect x="3" y="4" width="6" height="16" rx="1" />
1497
- <rect x="11" y="4" width="4" height="16" rx="1" />
1498
- <rect x="17" y="4" width="4" height="16" rx="1" />
1499
- </svg>
1533
+ {@render icon("tool-panel")}
1500
1534
  Columns &amp; Filters
1501
1535
  </button>
1502
1536
  {/if}
@@ -1509,9 +1543,7 @@
1509
1543
  aria-expanded={ctrl.chartPanelOpen}
1510
1544
  onclick={() => (ctrl.chartPanelOpen = !ctrl.chartPanelOpen)}
1511
1545
  >
1512
- <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1513
- <line x1="12" y1="20" x2="12" y2="10" /><line x1="18" y1="20" x2="18" y2="4" /><line x1="6" y1="20" x2="6" y2="16" />
1514
- </svg>
1546
+ {@render icon("chart")}
1515
1547
  Chart
1516
1548
  </button>
1517
1549
  {/if}
@@ -1519,7 +1551,7 @@
1519
1551
  {/if}
1520
1552
 
1521
1553
  {#if hasMeasured && (opt.paginationPosition === "top" || opt.paginationPosition === "both")}
1522
- <GridFooter {ctrl} showStatus={false} top pager pageSizeOptions={opt.pageSizeOptions} />
1554
+ <GridFooter {ctrl} {icon} showStatus={false} top pager pageSizeOptions={opt.pageSizeOptions} />
1523
1555
  {/if}
1524
1556
 
1525
1557
  <div
@@ -1613,20 +1645,9 @@
1613
1645
  <span class="sv-grid-group-header-label"
1614
1646
  >{cell.label}</span
1615
1647
  >
1616
- <svg
1617
- class="sv-grid-group-caret"
1618
- viewBox="0 0 16 16"
1619
- width="10"
1620
- height="10"
1621
- fill="none"
1622
- stroke="currentColor"
1623
- stroke-width="2.5"
1624
- stroke-linecap="round"
1625
- stroke-linejoin="round"
1626
- aria-hidden="true"
1648
+ <span class="sv-grid-group-caret" aria-hidden="true"
1649
+ >{@render icon("column-group-caret")}</span
1627
1650
  >
1628
- <polyline points="4 6 8 10 12 6"></polyline>
1629
- </svg>
1630
1651
  </button>
1631
1652
  {:else}
1632
1653
  <span class="sv-grid-group-header-label"
@@ -1659,7 +1680,9 @@
1659
1680
  style={`width: ${rowNumberColumnWidth}px; min-width: ${rowNumberColumnWidth}px; max-width: ${rowNumberColumnWidth}px; left: 0;`}
1660
1681
  aria-label="Row number"
1661
1682
  >
1662
- <span class="sv-grid-row-number-head">#</span>
1683
+ <span class="sv-grid-row-number-head"
1684
+ >{@render icon("row-number")}</span
1685
+ >
1663
1686
  </th>
1664
1687
  {/if}
1665
1688
  {#if showRowSelectionEffective}
@@ -1980,7 +2003,7 @@
1980
2003
  onmousedown={(event) => {
1981
2004
  event.preventDefault();
1982
2005
  removeFilterChip(rendered.column.id, token);
1983
- }}>×</button
2006
+ }}>{@render icon("remove")}</button
1984
2007
  >
1985
2008
  </span>
1986
2009
  {/each}
@@ -2819,32 +2842,14 @@
2819
2842
  {/if}
2820
2843
  </div>
2821
2844
 
2822
- <GridFooter {ctrl} pager={(opt.paginationPosition ?? "bottom") !== "top"} pageSizeOptions={opt.pageSizeOptions} />
2845
+ <GridFooter {ctrl} {icon} pager={(opt.paginationPosition ?? "bottom") !== "top"} pageSizeOptions={opt.pageSizeOptions} />
2823
2846
 
2824
2847
  {#if ctrl.findOpen}
2825
2848
  <!-- Find-in-grid overlay. Anchored to the TOP of the grid root so
2826
2849
  it tracks the grid even when the page scrolls. Ctrl+F opens;
2827
2850
  Enter cycles to the next hit; Esc closes. -->
2828
2851
  <div class="sv-grid-find" role="search" aria-label="Find in grid">
2829
- <svg class="sv-grid-find-icon" viewBox="0 0 16 16" aria-hidden="true">
2830
- <circle
2831
- cx="7"
2832
- cy="7"
2833
- r="4.5"
2834
- fill="none"
2835
- stroke="currentColor"
2836
- stroke-width="1.5"
2837
- />
2838
- <line
2839
- x1="10.2"
2840
- y1="10.2"
2841
- x2="14"
2842
- y2="14"
2843
- stroke="currentColor"
2844
- stroke-width="1.5"
2845
- stroke-linecap="round"
2846
- />
2847
- </svg>
2852
+ {@render icon("search")}
2848
2853
  <input
2849
2854
  class="sv-grid-find-input"
2850
2855
  type="search"
@@ -2897,7 +2902,7 @@
2897
2902
  setActiveCell(hit.rowIndex, hit.colIndex);
2898
2903
  scrollActiveCellIntoView(hit.rowIndex, hit.colIndex);
2899
2904
  }
2900
- }}>↑</button
2905
+ }}>{@render icon("find-prev")}</button
2901
2906
  >
2902
2907
  <button
2903
2908
  type="button"
@@ -2911,7 +2916,7 @@
2911
2916
  setActiveCell(hit.rowIndex, hit.colIndex);
2912
2917
  scrollActiveCellIntoView(hit.rowIndex, hit.colIndex);
2913
2918
  }
2914
- }}>↓</button
2919
+ }}>{@render icon("find-next")}</button
2915
2920
  >
2916
2921
  <button
2917
2922
  type="button"
@@ -2922,7 +2927,7 @@
2922
2927
  ctrl.findQuery = "";
2923
2928
  // Return focus to the grid instead of letting it fall to <body> (#80).
2924
2929
  ctrl.gridRootEl?.focus({ preventScroll: true });
2925
- }}>✕</button
2930
+ }}>{@render icon("close")}</button
2926
2931
  >
2927
2932
  </div>
2928
2933
  {/if}
@@ -2960,7 +2965,7 @@
2960
2965
  type="button"
2961
2966
  class="sv-grid-tool-panel-close"
2962
2967
  aria-label="Close"
2963
- onclick={() => (ctrl.toolPanelOpen = false)}>✕</button
2968
+ onclick={() => (ctrl.toolPanelOpen = false)}>{@render icon("close")}</button
2964
2969
  >
2965
2970
  </div>
2966
2971
  <div class="sv-grid-tool-panel-tabs" role="tablist">
@@ -3004,21 +3009,21 @@
3004
3009
  class:is-active={grouped}
3005
3010
  aria-label={grouped ? "Ungroup" : "Group by"}
3006
3011
  title={grouped ? "Ungroup" : "Group by this column"}
3007
- onclick={() => toggleGroupInPanel(column.id)}>⊞</button
3012
+ onclick={() => toggleGroupInPanel(column.id)}>{@render icon("group-add")}</button
3008
3013
  >
3009
3014
  <button
3010
3015
  type="button"
3011
3016
  class="sv-grid-tool-panel-btn"
3012
3017
  aria-label="Move up"
3013
3018
  disabled={i === 0}
3014
- onclick={() => moveColumnInPanel(column.id, -1)}>↑</button
3019
+ onclick={() => moveColumnInPanel(column.id, -1)}>{@render icon("move-up")}</button
3015
3020
  >
3016
3021
  <button
3017
3022
  type="button"
3018
3023
  class="sv-grid-tool-panel-btn"
3019
3024
  aria-label="Move down"
3020
3025
  disabled={i === toolPanelColumns.length - 1}
3021
- onclick={() => moveColumnInPanel(column.id, 1)}>↓</button
3026
+ onclick={() => moveColumnInPanel(column.id, 1)}>{@render icon("move-down")}</button
3022
3027
  >
3023
3028
  </span>
3024
3029
  </li>
@@ -3050,7 +3055,7 @@
3050
3055
  class="sv-grid-tp-filter-clear"
3051
3056
  aria-label={`Clear ${toolPanelHeaderLabel(column)} filter`}
3052
3057
  title={messages.clearFilter}
3053
- onclick={() => clearColumnFilter(column.id)}>✕</button
3058
+ onclick={() => clearColumnFilter(column.id)}>{@render icon("clear")}</button
3054
3059
  >
3055
3060
  {/if}
3056
3061
  </div>
@@ -11,6 +11,7 @@ import type {
11
11
  import type { ConditionalFormat } from "./conditional-formatting";
12
12
  import type { GroupDisplayType } from "./group-display";
13
13
  import type { GridMessages } from "./grid-messages";
14
+ import type { GridIconName, GridIcons } from "./grid-icons";
14
15
  import type { GridPivotConfig } from "./pivot-view.svelte";
15
16
  import type { GridPredicateExpr } from "./filtering/predicate-expr";
16
17
  import type { MenuItem } from "./SvMenuList.svelte";
@@ -120,6 +121,13 @@ export type ChartingConfig<TData extends RowData = RowData> = {
120
121
  yScale?: "linear" | "log";
121
122
  timeAxis?: boolean;
122
123
  valueFormat?: "number" | "currency" | "percent" | "compact";
124
+ /** ISO 4217 code for `valueFormat: 'currency'` (`'EUR'`, `'JPY'`, ...).
125
+ * Without it the axis reads `$`, whatever the data is actually in. */
126
+ currency?: string;
127
+ /** BCP-47 locale for the chart's numbers. Defaults to `localization.locale`,
128
+ * so a grid that is already localized gets a localized chart without setting
129
+ * anything here; set it to format the chart differently from the grid. */
130
+ locale?: string | ReadonlyArray<string>;
123
131
  smooth?: boolean;
124
132
  orientation?: "vertical" | "horizontal";
125
133
  donut?: number | boolean;
@@ -421,6 +429,13 @@ export type ChartViewConfig<
421
429
  palette?: string[];
422
430
  /** Number format for the value axis, tooltips and data labels. */
423
431
  valueFormat?: ChartValueFormat;
432
+ /** ISO 4217 code for `valueFormat: 'currency'` (`'EUR'`, `'JPY'`, ...).
433
+ * Without it the axis reads `$`, whatever the data is actually in. */
434
+ currency?: string;
435
+ /** BCP-47 locale for the chart's numbers. Defaults to `localization.locale`,
436
+ * so a grid that is already localized gets a localized chart without setting
437
+ * anything here; set it to format the chart differently from the grid. */
438
+ locale?: string | ReadonlyArray<string>;
424
439
  /** Show the clickable legend. Default `true`. */
425
440
  legend?: boolean;
426
441
  /** Draw the value on each bar / point / slice. Default `false`. */
@@ -1330,6 +1345,25 @@ export type Props<TFeatures extends TableFeatures = TableFeatures, TData extends
1330
1345
  * Omitting it (or any field) is a no-op. See {@link GridMessages}.
1331
1346
  */
1332
1347
  localization?: GridLocalization;
1348
+ /**
1349
+ * Replace any of the grid's own icons with your own markup. One snippet per
1350
+ * icon name; names you leave out keep their built-in glyph, so a partial map
1351
+ * is all you ever need. `localization.text` for strings, this for glyphs.
1352
+ *
1353
+ * ```svelte
1354
+ * {#snippet funnel()}<MyFilterIcon />{/snippet}
1355
+ * <SvGrid icons={{ filter: funnel }} {data} {columns} />
1356
+ * ```
1357
+ *
1358
+ * Your markup is wrapped in the grid's own icon box, so it inherits the size
1359
+ * of the glyph it replaced and the rotation an expander applies when it
1360
+ * opens. Colour follows `currentColor`, as the built-ins do.
1361
+ *
1362
+ * See {@link GridIconName} for the catalogue, and the Icons guide for the
1363
+ * handful of marks this does not cover (the scrollbar arrows and the
1364
+ * checkbox tick, both for reasons documented in `grid-icons.ts`).
1365
+ */
1366
+ icons?: GridIcons;
1333
1367
  /**
1334
1368
  * Show the single search box that filters across every column. Explicitly
1335
1369
  * setting this wins over `filterMode`; leaving it unset means it appears only
@@ -2087,6 +2121,13 @@ export type FilterOperator =
2087
2121
  export type FilterOption = {
2088
2122
  value: FilterOperator;
2089
2123
  label: string;
2090
- iconName: string;
2124
+ /**
2125
+ * Which glyph the operator draws. A `GridIconName` rather than a bare string
2126
+ * so an operator cannot name an icon the grid has no case for: that is
2127
+ * exactly how `op-between` once rendered an empty `<svg>` in the operator
2128
+ * menu. `FilterOperator` is a closed union, so nothing user-supplied is
2129
+ * being narrowed here.
2130
+ */
2131
+ iconName: GridIconName;
2091
2132
  };
2092
2133
  export type MenuPosition = { x: number; y: number };