@shwfed/config 2.12.6 → 2.12.8

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 (31) hide show
  1. package/dist/module.json +1 -1
  2. package/dist/runtime/components/config/blocks/2026-06-17/com.shwfed.block.tree.single/runtime.vue +54 -50
  3. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.d.vue.ts +2 -0
  4. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue +38 -0
  5. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/config.vue.d.ts +2 -0
  6. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/runtime.vue +11 -2
  7. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/schema.d.ts +3 -0
  8. package/dist/runtime/components/form/fields/2026-04-28/com.shwfed.form.field.number/schema.js +7 -0
  9. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
  10. package/dist/runtime/components/form/fields/2026-05-23/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
  11. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
  12. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
  13. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
  14. package/dist/runtime/components/form/fields/2026-05-26/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
  15. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.d.vue.ts +18 -18
  16. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.multi/config.vue.d.ts +18 -18
  17. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.d.vue.ts +18 -18
  18. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.combobox.single/config.vue.d.ts +18 -18
  19. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.d.vue.ts +18 -18
  20. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.multi/config.vue.d.ts +18 -18
  21. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.d.vue.ts +18 -18
  22. package/dist/runtime/components/form/fields/2026-05-28/com.shwfed.form.field.tree.single/config.vue.d.ts +18 -18
  23. package/dist/runtime/components/table/columns/2026-04-14/com.shwfed.table.column.number/runtime.vue +6 -73
  24. package/dist/runtime/components/table/columns/2026-05-20/com.shwfed.table.column.number-input/config.vue +44 -0
  25. package/dist/runtime/components/table/columns/2026-05-20/com.shwfed.table.column.number-input/runtime.vue +8 -2
  26. package/dist/runtime/components/table/columns/2026-05-20/com.shwfed.table.column.number-input/schema.d.ts +3 -0
  27. package/dist/runtime/components/table/columns/2026-05-20/com.shwfed.table.column.number-input/schema.js +7 -0
  28. package/dist/runtime/components/table/index.vue +180 -168
  29. package/dist/runtime/share/number-format.d.ts +7 -0
  30. package/dist/runtime/share/number-format.js +72 -0
  31. package/package.json +1 -1
@@ -7,6 +7,7 @@ import { cel as _rawCel } from "../../../../../utils/cel";
7
7
  import { celScope, injectCELContext } from "../../../../../utils/cel-context";
8
8
  import { useFormReadonly } from "../../../../form/utils/readonly";
9
9
  import { getLocalizedText } from "../../../../../share/locale";
10
+ import { formatNumberDisplay } from "../../../../../share/number-format";
10
11
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupNumberField } from "../../../../ui/input-group";
11
12
  import ShwfedActions from "../../../../actions/components/group.vue";
12
13
  import { useFieldValue } from "../../../../form/utils/field-value";
@@ -70,6 +71,11 @@ const { draft, commit } = useFieldValue({
70
71
  return props.column.valueAsString ? String(next) : next;
71
72
  }
72
73
  });
74
+ const readonlyDisplay = computed(() => formatNumberDisplay(draft.value, {
75
+ displayMode: props.column.displayMode,
76
+ precision: props.column.precision,
77
+ roundingMode: props.column.roundingMode
78
+ }));
73
79
  function applyRounding(n, precision, mode) {
74
80
  const factor = 10 ** precision;
75
81
  switch (mode) {
@@ -133,10 +139,10 @@ async function onBlur() {
133
139
  v-if="effectiveReadonly"
134
140
  :class="[
135
141
  'flex items-center h-7 w-full px-2 text-[0.75rem] truncate',
136
- draft !== void 0 ? ['text-zinc-700', justifyClass] : 'justify-center font-mono text-zinc-300 select-none'
142
+ draft !== void 0 ? ['text-zinc-700', justifyClass, column.displayMode === 'monoThousand' && 'font-mono'] : 'justify-center font-mono text-zinc-300 select-none'
137
143
  ]"
138
144
  >
139
- {{ draft ?? "-" }}
145
+ {{ readonlyDisplay }}
140
146
  </span>
141
147
  <InputGroup
142
148
  v-else
@@ -33,6 +33,9 @@ export declare function schema(configure: (env: Environment) => void): Schema.St
33
33
  valueAsString: Schema.optional<Schema.SchemaClass<boolean, boolean, never>>;
34
34
  min: Schema.optional<Schema.Schema<string, string, never>>;
35
35
  max: Schema.optional<Schema.Schema<string, string, never>>;
36
+ displayMode: Schema.optionalWith<Schema.Literal<["plain", "uppercaseChinese", "monoThousand"]>, {
37
+ default: () => "plain";
38
+ }>;
36
39
  derived: Schema.optional<Schema.Struct<{
37
40
  mode: Schema.Literal<["formula", "prefill"]>;
38
41
  expression: Schema.Schema<string, string, never>;
@@ -68,6 +68,13 @@ export function schema(configure) {
68
68
  title: "\u6700\u5927\u503C",
69
69
  description: "\u5141\u8BB8\u8F93\u5165\u7684\u6700\u5927\u503C\u8868\u8FBE\u5F0F\uFF0C\u53EF\u8BBF\u95EE `row` / `index` \u548C `form`\uFF1B\u7559\u7A7A\u65F6\u4E0D\u9650\u5236\u4E0A\u9650"
70
70
  })),
71
+ displayMode: Schema.optionalWith(
72
+ Schema.Literal("plain", "uppercaseChinese", "monoThousand").annotations({
73
+ title: "\u6A21\u5F0F",
74
+ description: "\u53EA\u8BFB\u5C55\u793A\u65F6\u7684\u6A21\u5F0F\uFF1A\u6570\u503C\uFF08\u539F\u6837\uFF09\u3001\u4E2D\u6587\u5927\u5199\uFF08\u5982 4200 \u2192 `\u8086\u4EDF\u8D30\u4F70\u96F6\u62FE`\uFF09\u3001\u7B49\u5BBD\u5B57\u4F53\u52A0\u5343\u5206\u4F4D\uFF08\u5982 `4,200`\uFF09\uFF1B\u7F16\u8F91\u6001\u4E0D\u53D7\u5F71\u54CD"
75
+ }),
76
+ { default: () => "plain" }
77
+ ),
71
78
  derived: derivedRowField(configure, "number"),
72
79
  // `Schema.optional` rebuilds the struct's union member, and because the
73
80
  // `optionalWith`-default fields make `AddonActions` a `Transformation`, the
@@ -12,7 +12,7 @@ import {
12
12
  import { useVirtualizer } from "@tanstack/vue-virtual";
13
13
  import { Effect, Fiber, Option } from "effect";
14
14
  import { Fetch } from "fx-fetch";
15
- import { Pagination } from "reka-ui/namespaced";
15
+ import { Pagination, ScrollArea } from "reka-ui/namespaced";
16
16
  import { computed, h, onMounted, ref, toRaw, watch } from "vue";
17
17
  import { fetchJsonOption } from "../../share/request";
18
18
  import { useI18n } from "vue-i18n";
@@ -24,6 +24,7 @@ import { Markdown } from "../ui/markdown";
24
24
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
25
25
  import { NumberField, NumberFieldInput } from "../ui/number-field";
26
26
  import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
27
+ import { ScrollBar } from "../ui/scroll-area";
27
28
  import ShwfedActions from "../actions/components/group.vue";
28
29
  import ShwfedForm from "../form/index.vue";
29
30
  import { evaluateInitial } from "../form/utils/initial";
@@ -105,7 +106,8 @@ const { t, locale } = useI18n({
105
106
  }
106
107
  });
107
108
  const inheritedContext = injectCELContext();
108
- const containerRef = ref(null);
109
+ const viewportRef = ref(null);
110
+ const containerRef = computed(() => viewportRef.value?.viewportElement ?? null);
109
111
  const appliedInitialStateKey = ref("");
110
112
  const frozenGrowColumns = ref(/* @__PURE__ */ new Set());
111
113
  function isEffectiveGrow(column) {
@@ -788,29 +790,35 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
788
790
  @reset="resetCustomization"
789
791
  />
790
792
  <ClientOnly>
791
- <!-- Scrollable table container -->
792
- <div
793
- ref="containerRef"
794
- class="overflow-scroll overscroll-none flex-1 border border-zinc-200 rounded relative"
793
+ <!-- Scrollable table container: reka-ui ScrollArea replaces the native
794
+ scrollbar. The Viewport is the real scroll element (both axes, since
795
+ both ScrollBars are mounted) and the sticky thead / pinned columns
796
+ resolve against it. -->
797
+ <ScrollArea.Root
798
+ class="flex-1 min-h-0 border border-zinc-200 rounded relative overflow-hidden"
795
799
  >
796
- <div :style="{ height: `${rowTotalSize}px` }">
797
- <table class="grid">
798
- <!-- Header -->
799
- <thead
800
- v-if="columns.length > 0"
801
- class="grid sticky top-0 z-10 select-none"
802
- >
803
- <tr
804
- v-for="group in tableApi.getHeaderGroups()"
805
- :key="group.id"
806
- class="flex w-full border-b border-zinc-200"
800
+ <ScrollArea.Viewport
801
+ ref="viewportRef"
802
+ class="size-full overscroll-none"
803
+ >
804
+ <div :style="{ height: `${rowTotalSize}px` }">
805
+ <table class="grid">
806
+ <!-- Header -->
807
+ <thead
808
+ v-if="columns.length > 0"
809
+ class="grid sticky top-0 z-10 select-none"
807
810
  >
808
- <th
809
- v-for="header in group.headers"
810
- :key="header.id"
811
- :colspan="header.colSpan"
812
- :data-col-id="header.column.id"
813
- :class="[
811
+ <tr
812
+ v-for="group in tableApi.getHeaderGroups()"
813
+ :key="group.id"
814
+ class="flex w-full border-b border-zinc-200"
815
+ >
816
+ <th
817
+ v-for="header in group.headers"
818
+ :key="header.id"
819
+ :colspan="header.colSpan"
820
+ :data-col-id="header.column.id"
821
+ :class="[
814
822
  'flex items-center gap-2 border-zinc-300 py-2 text-zinc-600',
815
823
  'text-xs flex items-center justify-center relative',
816
824
  'bg-[color-mix(in_srgb,var(--primary)_7%,white)] group',
@@ -818,7 +826,7 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
818
826
  shouldHaveRightBorder(header.column) && 'border-r',
819
827
  shouldHaveLeftBorder(header.column) && 'border-l'
820
828
  ]"
821
- :style="{
829
+ :style="{
822
830
  // Grow until first resize (see freezeGrowColumns): fill the
823
831
  // remaining space, with `width` as the flex-basis so the
824
832
  // configured size acts as a floor. Group headers grow by
@@ -827,82 +835,82 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
827
835
  ...headerGrowStyle(header),
828
836
  ...pinnedStyle(header.column)
829
837
  }"
830
- >
831
- <!-- Column header with tooltip: dashed underline + Markdown tooltip -->
832
- <Tooltip
833
- v-if="!header.isPlaceholder && header.column.columnDef.meta?.tooltip"
834
- :delay-duration="180"
835
838
  >
836
- <TooltipTrigger as-child>
837
- <span class="decoration-dashed underline decoration-(--primary) underline-offset-4 px-4 cursor-help">
838
- <FlexRender
839
- :render="header.column.columnDef.header"
840
- :props="header.getContext()"
841
- />
842
- </span>
843
- </TooltipTrigger>
844
- <TooltipContent
845
- align="center"
846
- side="top"
847
- class="max-w-xs text-xs"
839
+ <!-- Column header with tooltip: dashed underline + Markdown tooltip -->
840
+ <Tooltip
841
+ v-if="!header.isPlaceholder && header.column.columnDef.meta?.tooltip"
842
+ :delay-duration="180"
848
843
  >
849
- <Markdown
850
- :source="header.column.columnDef.meta.tooltip"
851
- class="prose prose-xs prose-zinc"
852
- />
853
- </TooltipContent>
854
- </Tooltip>
844
+ <TooltipTrigger as-child>
845
+ <span class="decoration-dashed underline decoration-(--primary) underline-offset-4 px-4 cursor-help">
846
+ <FlexRender
847
+ :render="header.column.columnDef.header"
848
+ :props="header.getContext()"
849
+ />
850
+ </span>
851
+ </TooltipTrigger>
852
+ <TooltipContent
853
+ align="center"
854
+ side="top"
855
+ class="max-w-xs text-xs"
856
+ >
857
+ <Markdown
858
+ :source="header.column.columnDef.meta.tooltip"
859
+ class="prose prose-xs prose-zinc"
860
+ />
861
+ </TooltipContent>
862
+ </Tooltip>
855
863
 
856
- <!-- Column header without tooltip: plain render -->
857
- <FlexRender
858
- v-else-if="!header.isPlaceholder"
859
- :render="header.column.columnDef.header"
860
- :props="header.getContext()"
861
- />
864
+ <!-- Column header without tooltip: plain render -->
865
+ <FlexRender
866
+ v-else-if="!header.isPlaceholder"
867
+ :render="header.column.columnDef.header"
868
+ :props="header.getContext()"
869
+ />
862
870
 
863
- <!-- Edit affordance: marks an editable column, hidden when
871
+ <!-- Edit affordance: marks an editable column, hidden when
864
872
  the column reads as readonly (form-wide or its own
865
873
  `readonly` condition). -->
866
- <Icon
867
- v-if="!header.isPlaceholder && header.column.columnDef.meta?.editable && !isHeaderReadonly(header.column.columnDef.meta?.readonlyExpr)"
868
- icon="fluent:edit-20-regular"
869
- class="size-3 shrink-0 text-zinc-400"
870
- aria-hidden="true"
871
- />
874
+ <Icon
875
+ v-if="!header.isPlaceholder && header.column.columnDef.meta?.editable && !isHeaderReadonly(header.column.columnDef.meta?.readonlyExpr)"
876
+ icon="fluent:edit-20-regular"
877
+ class="size-3 shrink-0 text-zinc-400"
878
+ aria-hidden="true"
879
+ />
872
880
 
873
- <!-- Sort toggle -->
874
- <Tooltip
875
- v-if="!header.isPlaceholder && header.column.getCanSort()"
876
- :delay-duration="800"
877
- >
878
- <TooltipTrigger as-child>
879
- <Button
880
- variant="ghost"
881
- size="xs"
882
- :class="[
881
+ <!-- Sort toggle -->
882
+ <Tooltip
883
+ v-if="!header.isPlaceholder && header.column.getCanSort()"
884
+ :delay-duration="800"
885
+ >
886
+ <TooltipTrigger as-child>
887
+ <Button
888
+ variant="ghost"
889
+ size="xs"
890
+ :class="[
883
891
  'absolute hover:bg-transparent right-1 top-1/2 -translate-y-1/2 transform-3d transition-opacity duration-180',
884
892
  !header.column.getIsSorted() ? 'opacity-30 hover:opacity-60' : 'text-(--primary)/80 hover:text-(--primary)'
885
893
  ]"
886
- as-child
887
- >
888
- <button
889
- type="button"
890
- :aria-label="getSortLabel(header.column)"
891
- @click="header.column.getToggleSortingHandler()?.($event)"
894
+ as-child
892
895
  >
893
- <Icon :icon="getSortIcon(header.column)" />
894
- </button>
895
- </Button>
896
- </TooltipTrigger>
897
- <TooltipContent>
898
- {{ getSortLabel(header.column) }}
899
- </TooltipContent>
900
- </Tooltip>
896
+ <button
897
+ type="button"
898
+ :aria-label="getSortLabel(header.column)"
899
+ @click="header.column.getToggleSortingHandler()?.($event)"
900
+ >
901
+ <Icon :icon="getSortIcon(header.column)" />
902
+ </button>
903
+ </Button>
904
+ </TooltipTrigger>
905
+ <TooltipContent>
906
+ {{ getSortLabel(header.column) }}
907
+ </TooltipContent>
908
+ </Tooltip>
901
909
 
902
- <!-- Resize handle -->
903
- <div
904
- v-if="!header.isPlaceholder && header.column.getCanResize() && shouldShowResizeHandle(header.column)"
905
- :class="[
910
+ <!-- Resize handle -->
911
+ <div
912
+ v-if="!header.isPlaceholder && header.column.getCanResize() && shouldShowResizeHandle(header.column)"
913
+ :class="[
906
914
  'group',
907
915
  'absolute',
908
916
  'top-0',
@@ -918,40 +926,40 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
918
926
  'hover:opacity-100',
919
927
  'cursor-col-resize'
920
928
  ]"
921
- @mousedown="onResizeMouseDown(header, $event)"
922
- >
923
- <div class="w-2pt h-full translate-x-1pt transform-3d bg-[color-mix(in_srgb,var(--primary)_80%,white)]" />
924
- </div>
925
- </th>
926
- </tr>
927
- </thead>
929
+ @mousedown="onResizeMouseDown(header, $event)"
930
+ >
931
+ <div class="w-2pt h-full translate-x-1pt transform-3d bg-[color-mix(in_srgb,var(--primary)_80%,white)]" />
932
+ </div>
933
+ </th>
934
+ </tr>
935
+ </thead>
928
936
 
929
- <!-- Body -->
930
- <tbody
931
- v-if="columns.length > 0"
932
- class="grid relative"
933
- :style="{ height: `${rowTotalSize}px` }"
934
- >
935
- <template
936
- v-for="r in rowWindow"
937
- :key="rows[r.index]?.id ?? r.index"
937
+ <!-- Body -->
938
+ <tbody
939
+ v-if="columns.length > 0"
940
+ class="grid relative"
941
+ :style="{ height: `${rowTotalSize}px` }"
938
942
  >
939
- <TableRowProvider
940
- v-if="rows[r.index]"
941
- :source-index="rows[r.index].index"
942
- :display-index="getDisplayIndex(rows[r.index])"
943
- :derived-columns="derivedEditableColumns"
944
- :get-row="getRowAt"
945
- :set-row="setRowAt"
946
- :scope-id="config?.id"
943
+ <template
944
+ v-for="r in rowWindow"
945
+ :key="rows[r.index]?.id ?? r.index"
947
946
  >
948
- <tr
949
- :ref="getRowRefFn(rows[r.index].id)"
950
- class="flex absolute w-full border-b border-zinc-300"
951
- :data-index="rows[r.index].index"
952
- :style="{ transform: `translate3d(0, ${r.start}px, 0)` }"
947
+ <TableRowProvider
948
+ v-if="rows[r.index]"
949
+ :source-index="rows[r.index].index"
950
+ :display-index="getDisplayIndex(rows[r.index])"
951
+ :derived-columns="derivedEditableColumns"
952
+ :get-row="getRowAt"
953
+ :set-row="setRowAt"
954
+ :scope-id="config?.id"
953
955
  >
954
- <!--
956
+ <tr
957
+ :ref="getRowRefFn(rows[r.index].id)"
958
+ class="flex absolute w-full border-b border-zinc-300"
959
+ :data-index="rows[r.index].index"
960
+ :style="{ transform: `translate3d(0, ${r.start}px, 0)` }"
961
+ >
962
+ <!--
955
963
  Drop indicator. Absolutely positioned so it overlays
956
964
  the row without affecting flex layout, and sits at
957
965
  z-30 above sticky pinned cells (which use z-15) so
@@ -962,54 +970,54 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
962
970
  to the same pixel band, so the indicator doesn't
963
971
  jitter as the hitbox flips between them.
964
972
  -->
965
- <span
966
- v-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-above'"
967
- aria-hidden="true"
968
- class="absolute left-0 right-0 -top-px h-0.5 z-30 pointer-events-none bg-(--primary)"
969
- />
970
- <span
971
- v-else-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-below'"
972
- aria-hidden="true"
973
- class="absolute left-0 right-0 -bottom-px h-0.5 z-30 pointer-events-none bg-(--primary)"
974
- />
975
- <td
976
- v-for="cell in rows[r.index].getVisibleCells()"
977
- :key="cell.id"
978
- :class="[
973
+ <span
974
+ v-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-above'"
975
+ aria-hidden="true"
976
+ class="absolute left-0 right-0 -top-px h-0.5 z-30 pointer-events-none bg-(--primary)"
977
+ />
978
+ <span
979
+ v-else-if="rowReorder.instructionFor(rows[r.index].id) === 'reorder-below'"
980
+ aria-hidden="true"
981
+ class="absolute left-0 right-0 -bottom-px h-0.5 z-30 pointer-events-none bg-(--primary)"
982
+ />
983
+ <td
984
+ v-for="cell in rows[r.index].getVisibleCells()"
985
+ :key="cell.id"
986
+ :class="[
979
987
  'border-zinc-300',
980
988
  isEffectiveGrow(cell.column) && 'flex-[1_1_auto]',
981
989
  cell.column.getIsPinned() && 'sticky z-15',
982
990
  shouldHaveRightBorder(cell.column) && 'border-r',
983
991
  shouldHaveLeftBorder(cell.column) && 'border-l'
984
992
  ]"
985
- :style="{
993
+ :style="{
986
994
  width: `${cell.column.getSize()}px`,
987
995
  ...pinnedStyle(cell.column),
988
996
  ...getCellStyles(cell.getContext())
989
997
  }"
990
- >
991
- <FlexRender
992
- :render="cell.column.columnDef.cell"
993
- :props="cell.getContext()"
994
- />
995
- </td>
996
- </tr>
997
- </TableRowProvider>
998
- </template>
999
- </tbody>
998
+ >
999
+ <FlexRender
1000
+ :render="cell.column.columnDef.cell"
1001
+ :props="cell.getContext()"
1002
+ />
1003
+ </td>
1004
+ </tr>
1005
+ </TableRowProvider>
1006
+ </template>
1007
+ </tbody>
1000
1008
 
1001
- <!-- Footer -->
1002
- <tfoot class="hidden has-data-footer:grid sticky bottom-0 z-10 select-none border-t border-zinc-200">
1003
- <tr
1004
- v-for="group in tableApi.getFooterGroups()"
1005
- :key="group.id"
1006
- class="flex w-full border-zinc-200"
1007
- >
1008
- <th
1009
- v-for="header in group.headers"
1010
- :key="header.id"
1011
- :colspan="header.colSpan"
1012
- :class="[
1009
+ <!-- Footer -->
1010
+ <tfoot class="hidden has-data-footer:grid sticky bottom-0 z-10 select-none border-t border-zinc-200">
1011
+ <tr
1012
+ v-for="group in tableApi.getFooterGroups()"
1013
+ :key="group.id"
1014
+ class="flex w-full border-zinc-200"
1015
+ >
1016
+ <th
1017
+ v-for="header in group.headers"
1018
+ :key="header.id"
1019
+ :colspan="header.colSpan"
1020
+ :class="[
1013
1021
  'flex items-center gap-2 border-zinc-300 text-zinc-600 p-0',
1014
1022
  'text-xs flex items-center justify-center relative',
1015
1023
  'bg-[color-mix(in_srgb,var(--primary)_7%,white)] group',
@@ -1017,23 +1025,27 @@ export { TableConfig, createTableConfig, getColumnTechnicalKey } from "./schema"
1017
1025
  shouldHaveRightBorder(header.column) && 'border-r',
1018
1026
  shouldHaveLeftBorder(header.column) && 'border-l'
1019
1027
  ]"
1020
- :style="{
1028
+ :style="{
1021
1029
  width: `${header.getSize()}px`,
1022
1030
  ...headerGrowStyle(header),
1023
1031
  ...pinnedStyle(header.column)
1024
1032
  }"
1025
- >
1026
- <FlexRender
1027
- v-if="!header.isPlaceholder"
1028
- :render="header.column.columnDef.footer"
1029
- :props="header.getContext()"
1030
- />
1031
- </th>
1032
- </tr>
1033
- </tfoot>
1034
- </table>
1035
- </div>
1036
- </div>
1033
+ >
1034
+ <FlexRender
1035
+ v-if="!header.isPlaceholder"
1036
+ :render="header.column.columnDef.footer"
1037
+ :props="header.getContext()"
1038
+ />
1039
+ </th>
1040
+ </tr>
1041
+ </tfoot>
1042
+ </table>
1043
+ </div>
1044
+ </ScrollArea.Viewport>
1045
+ <ScrollBar orientation="vertical" />
1046
+ <ScrollBar orientation="horizontal" />
1047
+ <ScrollArea.Corner />
1048
+ </ScrollArea.Root>
1037
1049
  </ClientOnly>
1038
1050
 
1039
1051
  <!-- Pagination -->
@@ -0,0 +1,7 @@
1
+ export type NumberDisplayMode = 'plain' | 'uppercaseChinese' | 'monoThousand';
2
+ export declare function toChineseUppercase(n: number): string;
3
+ export declare function formatNumberDisplay(value: unknown, opts: {
4
+ displayMode?: NumberDisplayMode;
5
+ precision?: number;
6
+ roundingMode?: 'round' | 'floor' | 'ceil';
7
+ }): string;
@@ -0,0 +1,72 @@
1
+ const CHINESE_DIGITS = ["\u96F6", "\u58F9", "\u8D30", "\u53C1", "\u8086", "\u4F0D", "\u9646", "\u67D2", "\u634C", "\u7396"];
2
+ const CHINESE_UNITS = ["", "\u62FE", "\u4F70", "\u4EDF"];
3
+ const CHINESE_GROUP_UNITS = ["", "\u4E07", "\u4EBF", "\u5146"];
4
+ export function toChineseUppercase(n) {
5
+ if (Number.isNaN(n)) return "-";
6
+ if (n === 0) return "\u96F6";
7
+ const isNegative = n < 0;
8
+ const absN = Math.abs(n);
9
+ const [intPart, decPart] = String(absN).split(".");
10
+ let result = "";
11
+ const digits = intPart.split("").reverse();
12
+ let zeroFlag = false;
13
+ for (let i = 0; i < digits.length; i++) {
14
+ const d = Number(digits[i]);
15
+ const groupIndex = Math.floor(i / 4);
16
+ const posInGroup = i % 4;
17
+ if (d === 0) {
18
+ zeroFlag = true;
19
+ } else {
20
+ if (zeroFlag) {
21
+ result = CHINESE_DIGITS[0] + result;
22
+ zeroFlag = false;
23
+ }
24
+ result = CHINESE_DIGITS[d] + CHINESE_UNITS[posInGroup] + result;
25
+ }
26
+ if (posInGroup === 0 && groupIndex > 0 && !zeroFlag) {
27
+ result = (CHINESE_GROUP_UNITS[groupIndex] ?? "") + result;
28
+ }
29
+ }
30
+ if (decPart) {
31
+ result += "\u70B9";
32
+ for (const ch of decPart) {
33
+ result += CHINESE_DIGITS[Number(ch)];
34
+ }
35
+ }
36
+ return isNegative ? `\u8D1F${result}` : result;
37
+ }
38
+ export function formatNumberDisplay(value, opts) {
39
+ if (value === void 0 || value === null) return "-";
40
+ const n = Number(value);
41
+ if (Number.isNaN(n)) return "-";
42
+ const displayMode = opts.displayMode ?? "plain";
43
+ if (displayMode === "uppercaseChinese") {
44
+ return toChineseUppercase(n);
45
+ }
46
+ let result;
47
+ if (opts.precision !== void 0) {
48
+ const mode = opts.roundingMode ?? "round";
49
+ const digits = opts.precision;
50
+ const factor = 10 ** digits;
51
+ let processed;
52
+ switch (mode) {
53
+ case "floor":
54
+ processed = Math.floor(n * factor) / factor;
55
+ break;
56
+ case "ceil":
57
+ processed = Math.ceil(n * factor) / factor;
58
+ break;
59
+ default:
60
+ processed = Math.round(n * factor) / factor;
61
+ }
62
+ result = processed.toFixed(digits);
63
+ } else {
64
+ result = String(n);
65
+ }
66
+ if (displayMode === "monoThousand") {
67
+ const [integer, decimal] = result.split(".");
68
+ const formattedInt = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
69
+ result = decimal !== void 0 ? `${formattedInt}.${decimal}` : formattedInt;
70
+ }
71
+ return result;
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shwfed/config",
3
- "version": "2.12.6",
3
+ "version": "2.12.8",
4
4
  "description": "Configurable UI for SHWFED",
5
5
  "type": "module",
6
6
  "publishConfig": {