@dou-ui/dashboard 0.1.0-alpha.4 → 0.1.0-alpha.5

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 (73) hide show
  1. package/dist/chart/chart-settings.js.map +1 -1
  2. package/dist/chart/chart-settings.vue_vue_type_script_setup_true_lang.js +10 -8
  3. package/dist/chart/chart-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  4. package/dist/chart/chart.js.map +1 -1
  5. package/dist/chart/chart.vue_vue_type_script_setup_true_lang.js +19 -15
  6. package/dist/chart/chart.vue_vue_type_script_setup_true_lang.js.map +1 -1
  7. package/dist/chart/read-props.js +26 -0
  8. package/dist/chart/read-props.js.map +1 -0
  9. package/dist/dashboard/dashboard.js.map +1 -1
  10. package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js +27 -4
  11. package/dist/dashboard/dashboard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  12. package/dist/dashboard/error-boundary.js +64 -0
  13. package/dist/dashboard/error-boundary.js.map +1 -0
  14. package/dist/dashboard/frame-context.js.map +1 -1
  15. package/dist/dashboard/widget-body.js.map +1 -1
  16. package/dist/dashboard/widget-body.vue_vue_type_script_setup_true_lang.js +53 -12
  17. package/dist/dashboard/widget-body.vue_vue_type_script_setup_true_lang.js.map +1 -1
  18. package/dist/dashboard/widget-frame.js.map +1 -1
  19. package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js +4 -2
  20. package/dist/dashboard/widget-frame.vue_vue_type_script_setup_true_lang.js.map +1 -1
  21. package/dist/dashboard/widget-settings.js.map +1 -1
  22. package/dist/dashboard/widget-settings.vue_vue_type_script_setup_true_lang.js +31 -5
  23. package/dist/dashboard/widget-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/locale.js +6 -0
  26. package/dist/locale.js.map +1 -1
  27. package/dist/pivot/pivot-settings.js.map +1 -1
  28. package/dist/pivot/pivot-settings.vue_vue_type_script_setup_true_lang.js +6 -4
  29. package/dist/pivot/pivot-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  30. package/dist/pivot/pivot.js.map +1 -1
  31. package/dist/pivot/pivot.vue_vue_type_script_setup_true_lang.js +4 -2
  32. package/dist/pivot/pivot.vue_vue_type_script_setup_true_lang.js.map +1 -1
  33. package/dist/pivot/read-props.js +44 -0
  34. package/dist/pivot/read-props.js.map +1 -0
  35. package/dist/style.css +1 -1
  36. package/dist/types/chart/read-props.d.ts +3 -0
  37. package/dist/types/dashboard/dashboard.vue.d.ts +2 -0
  38. package/dist/types/dashboard/error-boundary.d.ts +12 -0
  39. package/dist/types/dashboard/frame-context.d.ts +2 -0
  40. package/dist/types/dashboard/types.d.ts +17 -0
  41. package/dist/types/dashboard/widget-body.vue.d.ts +2 -0
  42. package/dist/types/dashboard/widget-settings.vue.d.ts +4 -0
  43. package/dist/types/index.d.ts +3 -1
  44. package/dist/types/locale.d.ts +4 -0
  45. package/dist/types/pivot/read-props.d.ts +3 -0
  46. package/dist/types/widgets/index.d.ts +1 -1
  47. package/dist/types/widgets/read-props.d.ts +4 -0
  48. package/dist/types/widgets/shared.d.ts +20 -4
  49. package/dist/widgets/index.js +2 -2
  50. package/dist/widgets/index.js.map +1 -1
  51. package/dist/widgets/read-props.js +52 -0
  52. package/dist/widgets/read-props.js.map +1 -0
  53. package/dist/widgets/shared.js +27 -5
  54. package/dist/widgets/shared.js.map +1 -1
  55. package/dist/widgets/stat-settings.js.map +1 -1
  56. package/dist/widgets/stat-settings.vue_vue_type_script_setup_true_lang.js +12 -10
  57. package/dist/widgets/stat-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  58. package/dist/widgets/stat.js.map +1 -1
  59. package/dist/widgets/stat.vue_vue_type_script_setup_true_lang.js +10 -13
  60. package/dist/widgets/stat.vue_vue_type_script_setup_true_lang.js.map +1 -1
  61. package/dist/widgets/table-settings.js.map +1 -1
  62. package/dist/widgets/table-settings.vue_vue_type_script_setup_true_lang.js +6 -4
  63. package/dist/widgets/table-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  64. package/dist/widgets/table.js.map +1 -1
  65. package/dist/widgets/table.vue_vue_type_script_setup_true_lang.js +3 -1
  66. package/dist/widgets/table.vue_vue_type_script_setup_true_lang.js.map +1 -1
  67. package/dist/widgets/text-settings.js.map +1 -1
  68. package/dist/widgets/text-settings.vue_vue_type_script_setup_true_lang.js +5 -3
  69. package/dist/widgets/text-settings.vue_vue_type_script_setup_true_lang.js.map +1 -1
  70. package/dist/widgets/text.js.map +1 -1
  71. package/dist/widgets/text.vue_vue_type_script_setup_true_lang.js +5 -3
  72. package/dist/widgets/text.vue_vue_type_script_setup_true_lang.js.map +1 -1
  73. package/package.json +8 -8
@@ -3,6 +3,7 @@ import { useDashboardWidget } from "../widget/context.js";
3
3
  import { rowsOf } from "../widgets/shared.js";
4
4
  import settings_field_default from "../widgets/settings-field.js";
5
5
  import source_field_default from "../widgets/source-field.js";
6
+ import { pivotPropsOf } from "./read-props.js";
6
7
  import { DouSwitch } from "dou-ui";
7
8
  import { computed, createBlock, createCommentVNode, createElementBlock, createTextVNode, createVNode, defineComponent, mergeModels, openBlock, toDisplayString, unref, useModel, withCtx } from "vue";
8
9
  import { DouPivotFields } from "@dou-ui/pivot";
@@ -18,9 +19,10 @@ var pivot_settings_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
18
19
  emits: ["update:modelValue"],
19
20
  setup(__props) {
20
21
  const model = useModel(__props, "modelValue");
22
+ const view = computed(() => pivotPropsOf(model.value));
21
23
  const messages = useDashboardMessages();
22
24
  const { data } = useDashboardWidget();
23
- const rows = computed(() => rowsOf(data.value, model.value.path));
25
+ const rows = computed(() => rowsOf(data.value, view.value.path));
24
26
  const set = (patch) => model.value = {
25
27
  ...model.value,
26
28
  ...patch
@@ -28,7 +30,7 @@ var pivot_settings_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
28
30
  return (_ctx, _cache) => {
29
31
  return openBlock(), createElementBlock("div", _hoisted_1, [
30
32
  createVNode(source_field_default, {
31
- "model-value": model.value.source,
33
+ "model-value": view.value.source,
32
34
  "onUpdate:modelValue": _cache[0] || (_cache[0] = (source) => set({ source }))
33
35
  }, null, 8, ["model-value"]),
34
36
  rows.value.length ? (openBlock(), createBlock(settings_field_default, {
@@ -38,7 +40,7 @@ var pivot_settings_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
38
40
  default: withCtx(() => [createVNode(unref(DouPivotFields), {
39
41
  data: rows.value,
40
42
  fields: __props.fields,
41
- config: model.value.config,
43
+ config: view.value.config,
42
44
  drawer: false,
43
45
  "onUpdate:config": _cache[1] || (_cache[1] = (config) => set({ config }))
44
46
  }, null, 8, [
@@ -49,7 +51,7 @@ var pivot_settings_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ d
49
51
  _: 1
50
52
  }, 8, ["label"])) : createCommentVNode("", true),
51
53
  createVNode(unref(DouSwitch), {
52
- "model-value": !!model.value.filter,
54
+ "model-value": !!view.value.filter,
53
55
  "onUpdate:modelValue": _cache[2] || (_cache[2] = (filter) => set({ filter: filter || void 0 }))
54
56
  }, {
55
57
  default: withCtx(() => [createTextVNode(toDisplayString(unref(messages).pivotFilter), 1)]),
@@ -1 +1 @@
1
- {"version":3,"file":"pivot-settings.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createElementBlock","_createVNode","SourceField","_createBlock","SettingsField","_unref","_withCtx","_createCommentVNode","_createTextVNode","_toDisplayString"],"sources":["../../src/pivot/pivot-settings.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivotFields } from '@dou-ui/pivot'\nimport type { PivotField } from '@dou-ui/pivot'\nimport { DouSwitch } from 'dou-ui'\nimport { computed } from 'vue'\nimport { useDashboardMessages } from '../locale'\nimport { useDashboardWidget } from '../widget/context'\nimport SettingsField from '../widgets/settings-field.vue'\nimport { rowsOf } from '../widgets/shared'\nimport SourceField from '../widgets/source-field.vue'\nimport type { PivotWidgetProps } from './types'\n\ndefineOptions({ name: 'DouDashboardPivotSettings' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\nconst model = defineModel<PivotWidgetProps>({ required: true })\nconst messages = useDashboardMessages()\nconst { data } = useDashboardWidget()\nconst rows = computed(() => rowsOf(data.value, model.value.path))\nconst set = (patch: Partial<PivotWidgetProps>) => (model.value = { ...model.value, ...patch })\n</script>\n\n<template>\n <div class=\"dou-dashboard__settings\">\n <SourceField :model-value=\"model.source\" @update:model-value=\"(source) => set({ source })\" />\n <SettingsField v-if=\"rows.length\" :label=\"messages.field\">\n <DouPivotFields\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"model.config\"\n :drawer=\"false\"\n @update:config=\"(config) => set({ config })\"\n />\n </SettingsField>\n <DouSwitch\n :model-value=\"!!model.filter\"\n @update:model-value=\"(filter) => set({ filter: filter || undefined })\"\n >\n {{ messages.pivotFilter }}\n </DouSwitch>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;EAeA,MAAM,QAAQA,SAA6B,SAAA,YAAmB;EAC9D,MAAM,WAAW,qBAAqB;EACtC,MAAM,EAAE,SAAS,mBAAmB;EACpC,MAAM,OAAO,eAAe,OAAO,KAAK,OAAO,MAAM,MAAM,IAAI,CAAC;EAChE,MAAM,OAAO,UAAsC,MAAM,QAAQ;GAAE,GAAG,MAAM;GAAO,GAAG;EAAM;;GAI1F,OAAAC,UAAA,GAAAC,mBAiBM,OAjBN,YAiBM;IAhBJC,YAA6FC,sBAAA;KAA/E,eAAa,MAAA,MAAM;KAAS,uBAAkB,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAG,OAAM,CAAA;;IACjE,KAAA,MAAK,UAA1BH,UAAA,GAAAI,YAQgBC,wBAAA;;KARmB,OAAOC,MAAA,QAAA,CAAQ,CAAC;;KACjD,SAAAC,cAME,CANFL,YAMEI,MAAA,cAAA,GAAA;MALC,MAAM,KAAA;MACN,QAAQ,QAAA;MACR,QAAQ,MAAA,MAAM;MACd,QAAQ;MACR,mBAAa,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAG,OAAM,CAAA;;;;;;;;IAG5CJ,YAKYI,MAAA,SAAA,GAAA;KAJT,eAAW,CAAA,CAAI,MAAA,MAAM;KACrB,uBAAkB,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAA,QAAW,UAAU,KAAA,EAAS,CAAA;;KAElE,SAAAC,cAA0B,CAAvBE,gBAAAC,gBAAAJ,MAAA,QAAA,CAAQ,CAAC,WAAW,GAAA,CAAA,C"}
1
+ {"version":3,"file":"pivot-settings.vue_vue_type_script_setup_true_lang.js","names":["_useModel","_openBlock","_createElementBlock","_createVNode","SourceField","_createBlock","SettingsField","_unref","_withCtx","_createCommentVNode","_createTextVNode","_toDisplayString"],"sources":["../../src/pivot/pivot-settings.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivotFields } from '@dou-ui/pivot'\nimport type { PivotField } from '@dou-ui/pivot'\nimport { DouSwitch } from 'dou-ui'\nimport { computed } from 'vue'\nimport { useDashboardMessages } from '../locale'\nimport { useDashboardWidget } from '../widget/context'\nimport SettingsField from '../widgets/settings-field.vue'\nimport { rowsOf } from '../widgets/shared'\nimport SourceField from '../widgets/source-field.vue'\nimport type { PivotWidgetProps } from './types'\nimport { pivotPropsOf } from './read-props'\n\ndefineOptions({ name: 'DouDashboardPivotSettings' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\nconst model = defineModel<PivotWidgetProps>({ required: true })\n// the props as saved may have the wrong types: read them as the widget does\nconst view = computed(() => pivotPropsOf(model.value as Record<string, unknown>))\nconst messages = useDashboardMessages()\nconst { data } = useDashboardWidget()\nconst rows = computed(() => rowsOf(data.value, view.value.path))\nconst set = (patch: Partial<PivotWidgetProps>) => (model.value = { ...model.value, ...patch })\n</script>\n\n<template>\n <div class=\"dou-dashboard__settings\">\n <SourceField :model-value=\"view.source\" @update:model-value=\"(source) => set({ source })\" />\n <SettingsField v-if=\"rows.length\" :label=\"messages.field\">\n <DouPivotFields\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"view.config\"\n :drawer=\"false\"\n @update:config=\"(config) => set({ config })\"\n />\n </SettingsField>\n <DouSwitch\n :model-value=\"!!view.filter\"\n @update:model-value=\"(filter) => set({ filter: filter || undefined })\"\n >\n {{ messages.pivotFilter }}\n </DouSwitch>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;EAgBA,MAAM,QAAQA,SAA6B,SAAA,YAAmB;EAE9D,MAAM,OAAO,eAAe,aAAa,MAAM,KAAgC,CAAC;EAChF,MAAM,WAAW,qBAAqB;EACtC,MAAM,EAAE,SAAS,mBAAmB;EACpC,MAAM,OAAO,eAAe,OAAO,KAAK,OAAO,KAAK,MAAM,IAAI,CAAC;EAC/D,MAAM,OAAO,UAAsC,MAAM,QAAQ;GAAE,GAAG,MAAM;GAAO,GAAG;EAAM;;GAI1F,OAAAC,UAAA,GAAAC,mBAiBM,OAjBN,YAiBM;IAhBJC,YAA4FC,sBAAA;KAA9E,eAAa,KAAA,MAAK;KAAS,uBAAkB,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAG,OAAM,CAAA;;IAChE,KAAA,MAAK,UAA1BH,UAAA,GAAAI,YAQgBC,wBAAA;;KARmB,OAAOC,MAAA,QAAA,CAAQ,CAAC;;KACjD,SAAAC,cAME,CANFL,YAMEI,MAAA,cAAA,GAAA;MALC,MAAM,KAAA;MACN,QAAQ,QAAA;MACR,QAAQ,KAAA,MAAK;MACb,QAAQ;MACR,mBAAa,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAG,OAAM,CAAA;;;;;;;;IAG5CJ,YAKYI,MAAA,SAAA,GAAA;KAJT,eAAW,CAAA,CAAI,KAAA,MAAK;KACpB,uBAAkB,OAAA,OAAA,OAAA,MAAG,WAAW,IAAG,EAAA,QAAW,UAAU,KAAA,EAAS,CAAA;;KAElE,SAAAC,cAA0B,CAAvBE,gBAAAC,gBAAAJ,MAAA,QAAA,CAAQ,CAAC,WAAW,GAAA,CAAA,C"}
@@ -1 +1 @@
1
- {"version":3,"file":"pivot.js","names":["_sfc_main"],"sources":["../../src/pivot/pivot.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivot } from '@dou-ui/pivot'\nimport type {\n PivotCellEvent,\n PivotConfig,\n PivotDimension,\n PivotExpose,\n PivotField,\n} from '@dou-ui/pivot'\nimport { computed, shallowRef, watch } from 'vue'\nimport { useDashboardWidget } from '../widget/context'\nimport { filterValueOf, rowsOf } from '../widgets/shared'\nimport type { PivotWidgetProps } from './types'\n\ndefineOptions({ name: 'DouDashboardPivotWidget' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\n\nconst { props, data, size, setFilter, ownFilters, editing, setProps, setWorkbook } =\n useDashboardWidget<PivotWidgetProps>()\nconst pivot = shallowRef<PivotExpose>()\n// the export menu writes the table as it shows: merged heads, totals, number formats\nsetWorkbook(() => pivot.value?.toSheet({ name: '' }))\nconst rows = computed(() => rowsOf(data.value, props.value.path))\n// sorting in the table works while viewing too; only while editing is it saved\nconst config = shallowRef<PivotConfig | undefined>(props.value.config)\nwatch(\n () => props.value.config,\n (next) => (config.value = next),\n)\nfunction onConfig(next: PivotConfig) {\n config.value = next\n if (editing.value) setProps({ config: next }, { group: 'pivot-config' })\n}\nconst nameOf = (dimension: PivotDimension) =>\n typeof dimension === 'string' ? dimension : dimension.field\n\n/** The groups this widget's filters picked, marked in the table. */\nfunction pathOf(dimensions: readonly PivotDimension[] = [], filters: Record<string, unknown>) {\n const path: unknown[] = []\n for (const dimension of dimensions) {\n const value = filters[nameOf(dimension)]\n if (value === undefined) break\n path.push(value)\n }\n return path\n}\nconst highlight = computed(() => {\n if (!props.value.filter) return undefined\n const filters = ownFilters.value\n return {\n row: pathOf(config.value?.rows, filters),\n column: pathOf(config.value?.columns, filters),\n }\n})\n\n/** One filter per field of a group's path; the fields past its end are cleared. */\nfunction apply(\n dimensions: readonly PivotDimension[] = [],\n path: readonly unknown[],\n labels: string[],\n) {\n dimensions.forEach((dimension, i) =>\n i < path.length\n ? setFilter(nameOf(dimension), filterValueOf(path[i]), { label: labels[i] })\n : setFilter(nameOf(dimension), undefined),\n )\n}\n\n/** Sets one filter per field of the clicked cell's groups; a total clears the deeper ones. */\nfunction onCell(cell: PivotCellEvent) {\n if (!props.value.filter) return\n const { rows: rowDims, columns: columnDims } = config.value ?? {}\n apply(rowDims, cell.row, cell.labels.row)\n apply(columnDims, cell.column, cell.labels.column)\n}\n</script>\n\n<template>\n <DouPivot\n ref=\"pivot\"\n class=\"dou-dashboard-pivot\"\n :bordered=\"false\"\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"config\"\n :highlight=\"highlight\"\n size=\"sm\"\n :max-height=\"size.height.value || undefined\"\n @update:config=\"onConfig\"\n @cell-click=\"onCell\"\n />\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"pivot.js","names":["_sfc_main"],"sources":["../../src/pivot/pivot.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivot } from '@dou-ui/pivot'\nimport type {\n PivotCellEvent,\n PivotConfig,\n PivotDimension,\n PivotExpose,\n PivotField,\n} from '@dou-ui/pivot'\nimport { computed, shallowRef, watch } from 'vue'\nimport { useDashboardWidget } from '../widget/context'\nimport { filterValueOf, rowsOf } from '../widgets/shared'\nimport { pivotPropsOf } from './read-props'\n\ndefineOptions({ name: 'DouDashboardPivotWidget' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\n\nconst {\n props: raw,\n data,\n size,\n setFilter,\n ownFilters,\n editing,\n setProps,\n setWorkbook,\n} = useDashboardWidget()\nconst props = computed(() => pivotPropsOf(raw.value))\nconst pivot = shallowRef<PivotExpose>()\n// the export menu writes the table as it shows: merged heads, totals, number formats\nsetWorkbook(() => pivot.value?.toSheet({ name: '' }))\nconst rows = computed(() => rowsOf(data.value, props.value.path))\n// sorting in the table works while viewing too; only while editing is it saved\nconst config = shallowRef<PivotConfig | undefined>(props.value.config)\n// only a new config from the props replaces it, not a change to another prop\nwatch(\n () => raw.value.config,\n () => (config.value = props.value.config),\n)\nfunction onConfig(next: PivotConfig) {\n config.value = next\n if (editing.value) setProps({ config: next }, { group: 'pivot-config' })\n}\nconst nameOf = (dimension: PivotDimension) =>\n typeof dimension === 'string' ? dimension : dimension.field\n\n/** The groups this widget's filters picked, marked in the table. */\nfunction pathOf(dimensions: readonly PivotDimension[] = [], filters: Record<string, unknown>) {\n const path: unknown[] = []\n for (const dimension of dimensions) {\n const value = filters[nameOf(dimension)]\n if (value === undefined) break\n path.push(value)\n }\n return path\n}\nconst highlight = computed(() => {\n if (!props.value.filter) return undefined\n const filters = ownFilters.value\n return {\n row: pathOf(config.value?.rows, filters),\n column: pathOf(config.value?.columns, filters),\n }\n})\n\n/** One filter per field of a group's path; the fields past its end are cleared. */\nfunction apply(\n dimensions: readonly PivotDimension[] = [],\n path: readonly unknown[],\n labels: string[],\n) {\n dimensions.forEach((dimension, i) =>\n i < path.length\n ? setFilter(nameOf(dimension), filterValueOf(path[i]), { label: labels[i] })\n : setFilter(nameOf(dimension), undefined),\n )\n}\n\n/** Sets one filter per field of the clicked cell's groups; a total clears the deeper ones. */\nfunction onCell(cell: PivotCellEvent) {\n if (!props.value.filter) return\n const { rows: rowDims, columns: columnDims } = config.value ?? {}\n apply(rowDims, cell.row, cell.labels.row)\n apply(columnDims, cell.column, cell.labels.column)\n}\n</script>\n\n<template>\n <DouPivot\n ref=\"pivot\"\n class=\"dou-dashboard-pivot\"\n :bordered=\"false\"\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"config\"\n :highlight=\"highlight\"\n size=\"sm\"\n :max-height=\"size.height.value || undefined\"\n @update:config=\"onConfig\"\n @cell-click=\"onCell\"\n />\n</template>\n"],"mappings":""}
@@ -1,5 +1,6 @@
1
1
  import { useDashboardWidget } from "../widget/context.js";
2
2
  import { filterValueOf, rowsOf } from "../widgets/shared.js";
3
+ import { pivotPropsOf } from "./read-props.js";
3
4
  import { computed, createBlock, defineComponent, openBlock, shallowRef, unref, watch } from "vue";
4
5
  import { DouPivot } from "@dou-ui/pivot";
5
6
  // src/pivot/pivot.vue?vue&type=script&setup=true&lang.ts
@@ -8,12 +9,13 @@ var pivot_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComp
8
9
  __name: "pivot",
9
10
  props: { fields: {} },
10
11
  setup(__props) {
11
- const { props, data, size, setFilter, ownFilters, editing, setProps, setWorkbook } = useDashboardWidget();
12
+ const { props: raw, data, size, setFilter, ownFilters, editing, setProps, setWorkbook } = useDashboardWidget();
13
+ const props = computed(() => pivotPropsOf(raw.value));
12
14
  const pivot = shallowRef();
13
15
  setWorkbook(() => pivot.value?.toSheet({ name: "" }));
14
16
  const rows = computed(() => rowsOf(data.value, props.value.path));
15
17
  const config = shallowRef(props.value.config);
16
- watch(() => props.value.config, (next) => config.value = next);
18
+ watch(() => raw.value.config, () => config.value = props.value.config);
17
19
  function onConfig(next) {
18
20
  config.value = next;
19
21
  if (editing.value) setProps({ config: next }, { group: "pivot-config" });
@@ -1 +1 @@
1
- {"version":3,"file":"pivot.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createBlock","_unref"],"sources":["../../src/pivot/pivot.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivot } from '@dou-ui/pivot'\nimport type {\n PivotCellEvent,\n PivotConfig,\n PivotDimension,\n PivotExpose,\n PivotField,\n} from '@dou-ui/pivot'\nimport { computed, shallowRef, watch } from 'vue'\nimport { useDashboardWidget } from '../widget/context'\nimport { filterValueOf, rowsOf } from '../widgets/shared'\nimport type { PivotWidgetProps } from './types'\n\ndefineOptions({ name: 'DouDashboardPivotWidget' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\n\nconst { props, data, size, setFilter, ownFilters, editing, setProps, setWorkbook } =\n useDashboardWidget<PivotWidgetProps>()\nconst pivot = shallowRef<PivotExpose>()\n// the export menu writes the table as it shows: merged heads, totals, number formats\nsetWorkbook(() => pivot.value?.toSheet({ name: '' }))\nconst rows = computed(() => rowsOf(data.value, props.value.path))\n// sorting in the table works while viewing too; only while editing is it saved\nconst config = shallowRef<PivotConfig | undefined>(props.value.config)\nwatch(\n () => props.value.config,\n (next) => (config.value = next),\n)\nfunction onConfig(next: PivotConfig) {\n config.value = next\n if (editing.value) setProps({ config: next }, { group: 'pivot-config' })\n}\nconst nameOf = (dimension: PivotDimension) =>\n typeof dimension === 'string' ? dimension : dimension.field\n\n/** The groups this widget's filters picked, marked in the table. */\nfunction pathOf(dimensions: readonly PivotDimension[] = [], filters: Record<string, unknown>) {\n const path: unknown[] = []\n for (const dimension of dimensions) {\n const value = filters[nameOf(dimension)]\n if (value === undefined) break\n path.push(value)\n }\n return path\n}\nconst highlight = computed(() => {\n if (!props.value.filter) return undefined\n const filters = ownFilters.value\n return {\n row: pathOf(config.value?.rows, filters),\n column: pathOf(config.value?.columns, filters),\n }\n})\n\n/** One filter per field of a group's path; the fields past its end are cleared. */\nfunction apply(\n dimensions: readonly PivotDimension[] = [],\n path: readonly unknown[],\n labels: string[],\n) {\n dimensions.forEach((dimension, i) =>\n i < path.length\n ? setFilter(nameOf(dimension), filterValueOf(path[i]), { label: labels[i] })\n : setFilter(nameOf(dimension), undefined),\n )\n}\n\n/** Sets one filter per field of the clicked cell's groups; a total clears the deeper ones. */\nfunction onCell(cell: PivotCellEvent) {\n if (!props.value.filter) return\n const { rows: rowDims, columns: columnDims } = config.value ?? {}\n apply(rowDims, cell.row, cell.labels.row)\n apply(columnDims, cell.column, cell.labels.column)\n}\n</script>\n\n<template>\n <DouPivot\n ref=\"pivot\"\n class=\"dou-dashboard-pivot\"\n :bordered=\"false\"\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"config\"\n :highlight=\"highlight\"\n size=\"sm\"\n :max-height=\"size.height.value || undefined\"\n @update:config=\"onConfig\"\n @cell-click=\"onCell\"\n />\n</template>\n"],"mappings":";;;;;;;;;;EAkBA,MAAM,EAAE,OAAO,MAAM,MAAM,WAAW,YAAY,SAAS,UAAU,gBACnE,mBAAqC;EACvC,MAAM,QAAQ,WAAwB;EAEtC,kBAAkB,MAAM,OAAO,QAAQ,EAAE,MAAM,GAAG,CAAC,CAAC;EACpD,MAAM,OAAO,eAAe,OAAO,KAAK,OAAO,MAAM,MAAM,IAAI,CAAC;EAEhE,MAAM,SAAS,WAAoC,MAAM,MAAM,MAAM;EACrE,YACQ,MAAM,MAAM,SACjB,SAAU,OAAO,QAAQ,IAC5B;EACA,SAAS,SAAS,MAAmB;GACnC,OAAO,QAAQ;GACf,IAAI,QAAQ,OAAO,SAAS,EAAE,QAAQ,KAAK,GAAG,EAAE,OAAO,eAAe,CAAC;EACzE;EACA,MAAM,UAAU,cACd,OAAO,cAAc,WAAW,YAAY,UAAU;;EAGxD,SAAS,OAAO,aAAwC,CAAC,GAAG,SAAkC;GAC5F,MAAM,OAAkB,CAAC;GACzB,KAAK,MAAM,aAAa,YAAY;IAClC,MAAM,QAAQ,QAAQ,OAAO,SAAS;IACtC,IAAI,UAAU,KAAA,GAAW;IACzB,KAAK,KAAK,KAAK;GACjB;GACA,OAAO;EACT;EACA,MAAM,YAAY,eAAe;GAC/B,IAAI,CAAC,MAAM,MAAM,QAAQ,OAAO,KAAA;GAChC,MAAM,UAAU,WAAW;GAC3B,OAAO;IACL,KAAK,OAAO,OAAO,OAAO,MAAM,OAAO;IACvC,QAAQ,OAAO,OAAO,OAAO,SAAS,OAAO;GAC/C;EACF,CAAC;;EAGD,SAAS,MACP,aAAwC,CAAC,GACzC,MACA,QACA;GACA,WAAW,SAAS,WAAW,MAC7B,IAAI,KAAK,SACL,UAAU,OAAO,SAAS,GAAG,cAAc,KAAK,EAAE,GAAG,EAAE,OAAO,OAAO,GAAG,CAAC,IACzE,UAAU,OAAO,SAAS,GAAG,KAAA,CAAS,CAC5C;EACF;;EAGA,SAAS,OAAO,MAAsB;GACpC,IAAI,CAAC,MAAM,MAAM,QAAQ;GACzB,MAAM,EAAE,MAAM,SAAS,SAAS,eAAe,OAAO,SAAS,CAAC;GAChE,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,GAAG;GACxC,MAAM,YAAY,KAAK,QAAQ,KAAK,OAAO,MAAM;EACnD;;GAIE,OAAAA,UAAA,GAAAC,YAYEC,MAAA,QAAA,GAAA;IAXI,SAAA;IAAJ,KAAI;IACJ,OAAM;IACL,UAAU;IACV,MAAM,KAAA;IACN,QAAQ,QAAA;IACR,QAAQ,OAAA;IACR,WAAW,UAAA;IACZ,MAAK;IACJ,cAAYA,MAAA,IAAA,CAAI,CAAC,OAAO,SAAS,KAAA;IACjC,mBAAe;IACf,aAAY"}
1
+ {"version":3,"file":"pivot.vue_vue_type_script_setup_true_lang.js","names":["_openBlock","_createBlock","_unref"],"sources":["../../src/pivot/pivot.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { DouPivot } from '@dou-ui/pivot'\nimport type {\n PivotCellEvent,\n PivotConfig,\n PivotDimension,\n PivotExpose,\n PivotField,\n} from '@dou-ui/pivot'\nimport { computed, shallowRef, watch } from 'vue'\nimport { useDashboardWidget } from '../widget/context'\nimport { filterValueOf, rowsOf } from '../widgets/shared'\nimport { pivotPropsOf } from './read-props'\n\ndefineOptions({ name: 'DouDashboardPivotWidget' })\n\ndefineProps<{ fields?: readonly PivotField[] }>()\n\nconst {\n props: raw,\n data,\n size,\n setFilter,\n ownFilters,\n editing,\n setProps,\n setWorkbook,\n} = useDashboardWidget()\nconst props = computed(() => pivotPropsOf(raw.value))\nconst pivot = shallowRef<PivotExpose>()\n// the export menu writes the table as it shows: merged heads, totals, number formats\nsetWorkbook(() => pivot.value?.toSheet({ name: '' }))\nconst rows = computed(() => rowsOf(data.value, props.value.path))\n// sorting in the table works while viewing too; only while editing is it saved\nconst config = shallowRef<PivotConfig | undefined>(props.value.config)\n// only a new config from the props replaces it, not a change to another prop\nwatch(\n () => raw.value.config,\n () => (config.value = props.value.config),\n)\nfunction onConfig(next: PivotConfig) {\n config.value = next\n if (editing.value) setProps({ config: next }, { group: 'pivot-config' })\n}\nconst nameOf = (dimension: PivotDimension) =>\n typeof dimension === 'string' ? dimension : dimension.field\n\n/** The groups this widget's filters picked, marked in the table. */\nfunction pathOf(dimensions: readonly PivotDimension[] = [], filters: Record<string, unknown>) {\n const path: unknown[] = []\n for (const dimension of dimensions) {\n const value = filters[nameOf(dimension)]\n if (value === undefined) break\n path.push(value)\n }\n return path\n}\nconst highlight = computed(() => {\n if (!props.value.filter) return undefined\n const filters = ownFilters.value\n return {\n row: pathOf(config.value?.rows, filters),\n column: pathOf(config.value?.columns, filters),\n }\n})\n\n/** One filter per field of a group's path; the fields past its end are cleared. */\nfunction apply(\n dimensions: readonly PivotDimension[] = [],\n path: readonly unknown[],\n labels: string[],\n) {\n dimensions.forEach((dimension, i) =>\n i < path.length\n ? setFilter(nameOf(dimension), filterValueOf(path[i]), { label: labels[i] })\n : setFilter(nameOf(dimension), undefined),\n )\n}\n\n/** Sets one filter per field of the clicked cell's groups; a total clears the deeper ones. */\nfunction onCell(cell: PivotCellEvent) {\n if (!props.value.filter) return\n const { rows: rowDims, columns: columnDims } = config.value ?? {}\n apply(rowDims, cell.row, cell.labels.row)\n apply(columnDims, cell.column, cell.labels.column)\n}\n</script>\n\n<template>\n <DouPivot\n ref=\"pivot\"\n class=\"dou-dashboard-pivot\"\n :bordered=\"false\"\n :data=\"rows\"\n :fields=\"fields\"\n :config=\"config\"\n :highlight=\"highlight\"\n size=\"sm\"\n :max-height=\"size.height.value || undefined\"\n @update:config=\"onConfig\"\n @cell-click=\"onCell\"\n />\n</template>\n"],"mappings":";;;;;;;;;;;EAkBA,MAAM,EACJ,OAAO,KACP,MACA,MACA,WACA,YACA,SACA,UACA,gBACE,mBAAmB;EACvB,MAAM,QAAQ,eAAe,aAAa,IAAI,KAAK,CAAC;EACpD,MAAM,QAAQ,WAAwB;EAEtC,kBAAkB,MAAM,OAAO,QAAQ,EAAE,MAAM,GAAG,CAAC,CAAC;EACpD,MAAM,OAAO,eAAe,OAAO,KAAK,OAAO,MAAM,MAAM,IAAI,CAAC;EAEhE,MAAM,SAAS,WAAoC,MAAM,MAAM,MAAM;EAErE,YACQ,IAAI,MAAM,cACT,OAAO,QAAQ,MAAM,MAAM,MACpC;EACA,SAAS,SAAS,MAAmB;GACnC,OAAO,QAAQ;GACf,IAAI,QAAQ,OAAO,SAAS,EAAE,QAAQ,KAAK,GAAG,EAAE,OAAO,eAAe,CAAC;EACzE;EACA,MAAM,UAAU,cACd,OAAO,cAAc,WAAW,YAAY,UAAU;;EAGxD,SAAS,OAAO,aAAwC,CAAC,GAAG,SAAkC;GAC5F,MAAM,OAAkB,CAAC;GACzB,KAAK,MAAM,aAAa,YAAY;IAClC,MAAM,QAAQ,QAAQ,OAAO,SAAS;IACtC,IAAI,UAAU,KAAA,GAAW;IACzB,KAAK,KAAK,KAAK;GACjB;GACA,OAAO;EACT;EACA,MAAM,YAAY,eAAe;GAC/B,IAAI,CAAC,MAAM,MAAM,QAAQ,OAAO,KAAA;GAChC,MAAM,UAAU,WAAW;GAC3B,OAAO;IACL,KAAK,OAAO,OAAO,OAAO,MAAM,OAAO;IACvC,QAAQ,OAAO,OAAO,OAAO,SAAS,OAAO;GAC/C;EACF,CAAC;;EAGD,SAAS,MACP,aAAwC,CAAC,GACzC,MACA,QACA;GACA,WAAW,SAAS,WAAW,MAC7B,IAAI,KAAK,SACL,UAAU,OAAO,SAAS,GAAG,cAAc,KAAK,EAAE,GAAG,EAAE,OAAO,OAAO,GAAG,CAAC,IACzE,UAAU,OAAO,SAAS,GAAG,KAAA,CAAS,CAC5C;EACF;;EAGA,SAAS,OAAO,MAAsB;GACpC,IAAI,CAAC,MAAM,MAAM,QAAQ;GACzB,MAAM,EAAE,MAAM,SAAS,SAAS,eAAe,OAAO,SAAS,CAAC;GAChE,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,GAAG;GACxC,MAAM,YAAY,KAAK,QAAQ,KAAK,OAAO,MAAM;EACnD;;GAIE,OAAAA,UAAA,GAAAC,YAYEC,MAAA,QAAA,GAAA;IAXI,SAAA;IAAJ,KAAI;IACJ,OAAM;IACL,UAAU;IACV,MAAM,KAAA;IACN,QAAQ,QAAA;IACR,QAAQ,OAAA;IACR,WAAW,UAAA;IACZ,MAAK;IACJ,cAAYA,MAAA,IAAA,CAAI,CAAC,OAAO,SAAS,KAAA;IACjC,mBAAe;IACf,aAAY"}
@@ -0,0 +1,44 @@
1
+ import { flagOf, oneOf, recordOf, textOf } from "../widgets/shared.js";
2
+ // src/pivot/read-props.ts
3
+ /** A field on the rows or columns: its name, or an object with a text `field`. */
4
+ var isDimension = (item) => typeof item === "string" || typeof recordOf(item)?.field === "string";
5
+ /** A value: a field's name, or an object. */
6
+ var isValue = (item) => typeof item === "string" || recordOf(item) !== void 0;
7
+ var listOf = (value, keep) => Array.isArray(value) ? value.filter(keep) : void 0;
8
+ /**
9
+ * The pivot's config with the right shape at its top: lists where lists go,
10
+ * objects where objects go; what isn't counts as not set. Deeper mistakes,
11
+ * like a filter's `include` that isn't a list, are left to the widget's
12
+ * error state.
13
+ */
14
+ function configOf(value) {
15
+ const config = recordOf(value);
16
+ if (!config) return void 0;
17
+ const filters = recordOf(config.filters);
18
+ const sort = recordOf(config.sort);
19
+ const totals = recordOf(config.totals);
20
+ return {
21
+ rows: listOf(config.rows, isDimension),
22
+ columns: listOf(config.columns, isDimension),
23
+ values: listOf(config.values, isValue),
24
+ filters: filters ? Object.fromEntries(Object.entries(filters).filter(([, filter]) => recordOf(filter))) : void 0,
25
+ valuesOn: oneOf(config.valuesOn, ["columns", "rows"]),
26
+ sort: sort && typeof sort.valueIndex === "number" && oneOf(sort.order, ["asc", "desc"]) ? sort : void 0,
27
+ totals: totals ? {
28
+ ...typeof totals.rows === "boolean" ? { rows: totals.rows } : {},
29
+ ...typeof totals.columns === "boolean" ? { columns: totals.columns } : {}
30
+ } : void 0
31
+ };
32
+ }
33
+ /** The pivot widget's props as its type says; what isn't the right type counts as not set. */
34
+ function pivotPropsOf(props) {
35
+ return {
36
+ source: textOf(props.source),
37
+ path: textOf(props.path),
38
+ config: configOf(props.config),
39
+ filter: flagOf(props.filter)
40
+ };
41
+ }
42
+ export { pivotPropsOf };
43
+
44
+ //# sourceMappingURL=read-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-props.js","names":[],"sources":["../../src/pivot/read-props.ts"],"sourcesContent":["import type { PivotConfig, PivotDimension, PivotValue } from '@dou-ui/pivot'\nimport { flagOf, oneOf, recordOf, textOf } from '../widgets/shared'\nimport type { PivotWidgetProps } from './types'\n\n/** A field on the rows or columns: its name, or an object with a text `field`. */\nconst isDimension = (item: unknown): item is PivotDimension =>\n typeof item === 'string' || typeof recordOf(item)?.field === 'string'\n\n/** A value: a field's name, or an object. */\nconst isValue = (item: unknown): item is PivotValue =>\n typeof item === 'string' || recordOf(item) !== undefined\n\nconst listOf = <T>(value: unknown, keep: (item: unknown) => item is T): T[] | undefined =>\n Array.isArray(value) ? value.filter(keep) : undefined\n\n/**\n * The pivot's config with the right shape at its top: lists where lists go,\n * objects where objects go; what isn't counts as not set. Deeper mistakes,\n * like a filter's `include` that isn't a list, are left to the widget's\n * error state.\n */\nfunction configOf(value: unknown): PivotConfig | undefined {\n const config = recordOf(value)\n if (!config) return undefined\n const filters = recordOf(config.filters)\n const sort = recordOf(config.sort)\n const totals = recordOf(config.totals)\n return {\n rows: listOf(config.rows, isDimension),\n columns: listOf(config.columns, isDimension),\n values: listOf(config.values, isValue),\n filters: filters\n ? (Object.fromEntries(\n Object.entries(filters).filter(([, filter]) => recordOf(filter)),\n ) as PivotConfig['filters'])\n : undefined,\n valuesOn: oneOf(config.valuesOn, ['columns', 'rows'] as const),\n sort:\n sort && typeof sort.valueIndex === 'number' && oneOf(sort.order, ['asc', 'desc'] as const)\n ? (sort as unknown as PivotConfig['sort'])\n : undefined,\n totals: totals\n ? {\n ...(typeof totals.rows === 'boolean' ? { rows: totals.rows } : {}),\n ...(typeof totals.columns === 'boolean' ? { columns: totals.columns } : {}),\n }\n : undefined,\n }\n}\n\n/** The pivot widget's props as its type says; what isn't the right type counts as not set. */\nexport function pivotPropsOf(props: Record<string, unknown>): PivotWidgetProps {\n return {\n source: textOf(props.source),\n path: textOf(props.path),\n config: configOf(props.config),\n filter: flagOf(props.filter),\n }\n}\n"],"mappings":";;;AAKA,IAAM,eAAe,SACnB,OAAO,SAAS,YAAY,OAAO,SAAS,IAAI,CAAC,EAAE,UAAU;;AAG/D,IAAM,WAAW,SACf,OAAO,SAAS,YAAY,SAAS,IAAI,MAAM,KAAA;AAEjD,IAAM,UAAa,OAAgB,SACjC,MAAM,QAAQ,KAAK,IAAI,MAAM,OAAO,IAAI,IAAI,KAAA;;;;;;;AAQ9C,SAAS,SAAS,OAAyC;CACzD,MAAM,SAAS,SAAS,KAAK;CAC7B,IAAI,CAAC,QAAQ,OAAO,KAAA;CACpB,MAAM,UAAU,SAAS,OAAO,OAAO;CACvC,MAAM,OAAO,SAAS,OAAO,IAAI;CACjC,MAAM,SAAS,SAAS,OAAO,MAAM;CACrC,OAAO;EACL,MAAM,OAAO,OAAO,MAAM,WAAW;EACrC,SAAS,OAAO,OAAO,SAAS,WAAW;EAC3C,QAAQ,OAAO,OAAO,QAAQ,OAAO;EACrC,SAAS,UACJ,OAAO,YACN,OAAO,QAAQ,OAAO,CAAC,CAAC,QAAQ,GAAG,YAAY,SAAS,MAAM,CAAC,CACjE,IACA,KAAA;EACJ,UAAU,MAAM,OAAO,UAAU,CAAC,WAAW,MAAM,CAAU;EAC7D,MACE,QAAQ,OAAO,KAAK,eAAe,YAAY,MAAM,KAAK,OAAO,CAAC,OAAO,MAAM,CAAU,IACpF,OACD,KAAA;EACN,QAAQ,SACJ;GACE,GAAI,OAAO,OAAO,SAAS,YAAY,EAAE,MAAM,OAAO,KAAK,IAAI,CAAC;GAChE,GAAI,OAAO,OAAO,YAAY,YAAY,EAAE,SAAS,OAAO,QAAQ,IAAI,CAAC;EAC3E,IACA,KAAA;CACN;AACF;;AAGA,SAAgB,aAAa,OAAkD;CAC7E,OAAO;EACL,QAAQ,OAAO,MAAM,MAAM;EAC3B,MAAM,OAAO,MAAM,IAAI;EACvB,QAAQ,SAAS,MAAM,MAAM;EAC7B,QAAQ,OAAO,MAAM,MAAM;CAC7B;AACF"}
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- @layer dou.components{.dou-grid-layout{--dou-grid-item-radius:var(--dou-radius-container);--dou-grid-col:calc((100cqw - (var(--dou-grid-columns) - 1)*var(--dou-grid-gap-x))/var(--dou-grid-columns));position:relative;box-sizing:border-box;height:calc(var(--dou-grid-rows)*var(--dou-grid-row) + max(var(--dou-grid-rows) - 1,0)*var(--dou-grid-gap-y));container-type:inline-size;width:100%;min-width:0}.dou-grid-layout.is-pointer-dragging{cursor:grabbing}.dou-grid-layout.is-resizing{cursor:nwse-resize}.dou-grid-layout__item,.dou-grid-layout__placeholder{position:absolute;box-sizing:border-box;width:calc(var(--dou-grid-w)*var(--dou-grid-col) + (var(--dou-grid-w) - 1)*var(--dou-grid-gap-x) + var(--dou-grid-dw,0px));height:calc(var(--dou-grid-h)*var(--dou-grid-row) + (var(--dou-grid-h) - 1)*var(--dou-grid-gap-y) + var(--dou-grid-dh,0px));border-radius:var(--dou-grid-item-radius);transform:translate(calc(var(--dou-grid-x)*(var(--dou-grid-col) + var(--dou-grid-gap-x)) + var(--dou-grid-dx,0px)),calc(var(--dou-grid-y)*(var(--dou-grid-row) + var(--dou-grid-gap-y)) + var(--dou-grid-dy,0px)));top:0;left:0}.dou-grid-layout__item{display:grid;grid-template:minmax(0,1fr)/minmax(0,1fr);outline:none;min-width:0}.dou-grid-layout__item:focus-visible{outline:2px solid var(--dou-color-focus);outline-offset:2px}.dou-grid-layout.is-editable .dou-grid-layout__item.is-movable{user-select:none;-webkit-touch-callout:none}.dou-grid-layout.is-editable:not(:-webkit-any(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-editable:not(:-moz-any(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-editable:not(:is(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-arranging .dou-grid-layout__item{transition:transform var(--dou-motion-duration-normal)var(--dou-motion-easing)}.dou-grid-layout__item.is-dragging,.dou-grid-layout__item.is-resizing{z-index:2;box-shadow:var(--dou-shadow-lg)}.dou-grid-layout.is-pointer-dragging .dou-grid-layout__item.is-dragging,.dou-grid-layout.is-pointer-dragging .dou-grid-layout__item.is-resizing{transition:none}.dou-grid-layout__placeholder{z-index:1;border:1px dashed var(--dou-color-border-strong);background-color:color-mix(in oklab,var(--dou-color-text)4%,transparent);pointer-events:none}.dou-grid-layout.is-arranging .dou-grid-layout__placeholder{transition:transform var(--dou-motion-duration-fast)var(--dou-motion-easing)}.dou-grid-layout__resize{position:absolute;z-index:1;touch-action:none}.dou-grid-layout__resize.is-e{cursor:ew-resize;width:8px;top:8px;bottom:16px;right:-4px}.dou-grid-layout__resize.is-s{cursor:ns-resize;height:8px;bottom:-4px;left:8px;right:16px}.dou-grid-layout__resize.is-se{cursor:nwse-resize;opacity:0;transition:opacity var(--dou-motion-duration-fast)var(--dou-motion-easing);width:20px;height:20px;bottom:0;right:0}.dou-grid-layout__resize.is-se:after{position:absolute;box-sizing:border-box;border-right:2px solid var(--dou-color-text-tertiary);border-bottom:2px solid var(--dou-color-text-tertiary);content:"";border-bottom-right-radius:4px;width:8px;height:8px;bottom:5px;right:5px}.dou-grid-layout__item:hover>.dou-grid-layout__resize.is-se,.dou-grid-layout__item.is-resizing>.dou-grid-layout__resize.is-se{opacity:1}.dou-grid-layout__item:focus-visible>.dou-grid-layout__resize.is-se{opacity:1}@media (pointer:coarse){.dou-grid-layout__resize.is-se{opacity:1;width:32px;height:32px}}.dou-grid-layout__live{position:absolute;overflow:hidden;clip-path:inset(50%);white-space:nowrap;width:1px;height:1px}}@layer dou.components{.dou-dashboard{position:relative;outline:none;min-width:0}.dou-dashboard__widget{display:flex;box-sizing:border-box;border:1px solid var(--dou-color-border);border-radius:inherit;background-color:var(--dou-color-bg-surface);color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm);overflow:hidden;flex-direction:column;min-width:0;min-height:0}.dou-grid-layout__item.is-dragging>.dou-dashboard__widget,.dou-grid-layout__item.is-resizing>.dou-dashboard__widget{border-color:var(--dou-color-border-strong)}.dou-dashboard__widget.has-peers{outline:1.5px solid var(--dou-dashboard-peer-color);outline-offset:2px}.dou-dashboard__peers{position:absolute;z-index:1;display:flex;overflow:hidden;pointer-events:none;gap:4px;max-width:calc(100% - 24px);margin-bottom:-10px;bottom:100%;left:12px}.dou-dashboard__peer{background-color:var(--dou-dashboard-peer-color);color:oklch(from var(--dou-dashboard-peer-color)clamp(.2,(.66 - l)*100,.99)0 0);font-size:var(--dou-font-size-sm);white-space:nowrap;border-radius:4px;flex:none;padding:0 6px;line-height:18px}.dou-dashboard__peer.is-focus{animation:dou-dashboard-peer-name var(--dou-motion-duration-fast)var(--dou-motion-easing)3s forwards}.dou-dashboard__header{display:flex;flex:none;align-items: center;gap:8px;height:40px;padding:0 8px 0 16px}.dou-dashboard__title{overflow:hidden;color:var(--dou-color-text);font-size:var(--dou-font-size-sm);font-weight:var(--dou-font-weight-medium);text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:min(6em,40%)}.dou-dashboard__title .dou-editable__view{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:inherit}.dou-dashboard__links{display:flex;overflow:hidden;flex:0 auto;gap:4px;min-width:0;max-width:60%}.dou-dashboard__links .dou-tag{min-width:0;max-width:100%}.dou-dashboard__links .dou-tag__label{overflow-x:clip;white-space:nowrap;text-overflow:ellipsis;min-width:0}.dou-dashboard__actions{display:flex;color:var(--dou-color-text-tertiary);flex:none;align-items: center;gap:2px}.dou-dashboard__comment{font-variant-numeric:tabular-nums}.dou-dashboard__comment:not(.has-threads){color:var(--dou-color-text-secondary)}@media (pointer:coarse){.dou-dashboard__comment.has-threads:after{position:absolute;content:"";inset:min(0px,50% - 20px)}}.dou-dashboard__fullscreen{display:flex;flex-direction:column;height:100%;min-height:0}.dou-dashboard__fullscreen>.dou-dashboard__body.is-fullscreen{flex:1;min-height:0}.dou-dashboard__fullscreen-bar{display:flex;justify-content:flex-end;margin-bottom:8px}.dou-dashboard__comment-card{box-sizing:border-box;max-height:min(var(--dou-floating-available,100vh),480px,calc(100vh - 32px));overflow-y:auto;overscroll-behavior:contain;border:1px solid var(--dou-color-border);border-radius:var(--dou-radius-container);background-color:color-mix(in oklab,var(--dou-color-text)calc(4%*var(--dou-scheme-dark)),var(--dou-color-bg-surface));box-shadow:var(--dou-shadow-md);width:min(340px,100vw - 32px);padding:12px}.dou-dashboard__comment-card>.dou-comment-thread+.dou-comment-thread{border-top:1px solid var(--dou-color-border-subtle);margin-top:12px;padding-top:12px}.dou-dashboard__export-error{flex:none;padding:0 8px 8px}.dou-dashboard__export-error .dou-alert__description{overflow-y:auto;max-height:4.5lh}.dou-dashboard__body{position:relative;flex:auto;min-width:0;min-height:0;padding:0 16px 16px}.dou-dashboard__body.is-fullscreen{height:100%;padding:0}.dou-dashboard__content{min-width:0;height:100%}.dou-dashboard__notice{display:grid;color:var(--dou-color-text-tertiary);text-align:center;place-items: center;height:100%}.dou-dashboard__status{position:absolute;display:flex;background-color:var(--dou-color-bg-surface);color:var(--dou-color-text-secondary);text-align:center;flex-direction:column;justify-content:center;align-items: center;gap:8px;padding:0 16px 16px;inset:0}.dou-dashboard__status.is-loading{background-color:color-mix(in oklab,var(--dou-color-bg-surface)72%,transparent);animation:dou-dashboard-fade-in var(--dou-motion-duration-fast)var(--dou-motion-easing).12s both}.dou-dashboard__status-icon{color:var(--dou-color-danger);width:20px;height:20px}.dou-dashboard__empty{position:absolute;display:grid;border:1px dashed var(--dou-color-border);border-radius:var(--dou-radius-container);pointer-events:none;place-items: center;inset:0}.dou-dashboard__live{position:absolute;overflow:hidden;clip-path:inset(50%);white-space:nowrap;width:1px;height:1px}.dou-dashboard__settings{display:flex;flex-direction:column;gap:16px}.dou-dashboard__field{display:flex;flex-direction:column;gap:8px}.dou-dashboard__field-hint{color:var(--dou-color-text-tertiary);font-size:var(--dou-font-size-sm)}.dou-dashboard__field-row{display:flex;flex-wrap:wrap;gap:16px}.dou-dashboard__field-row>.dou-dashboard__field{flex:120px;min-width:0}.dou-dashboard__columns{display:flex;flex-direction:column;gap:8px}.dou-dashboard__column{display:grid;grid-template-columns:minmax(0,1fr)minmax(0,1fr);align-items: center;gap:8px}.dou-dashboard__field-label{color:var(--dou-color-text);font-size:var(--dou-font-size-sm);font-weight:var(--dou-font-weight-medium)}@keyframes dou-dashboard-fade-in{0%{opacity:0}}.dou-dashboard-palette{display:flex;list-style:none;flex-direction:column;gap:8px;margin:0;padding:0}.dou-dashboard-palette.is-horizontal{flex-flow:wrap}.dou-dashboard-palette>li{margin:0}.dou-dashboard-palette__item{display:flex;box-sizing:border-box;border:1px solid var(--dou-color-border);border-radius:var(--dou-radius-control);background-color:var(--dou-color-bg-surface);color:var(--dou-color-text);font:inherit;font-size:var(--dou-font-size-sm);cursor:grab;user-select:none;-webkit-touch-callout:none;transition:border-color var(--dou-motion-duration-fast)var(--dou-motion-easing);align-items: center;gap:8px;width:100%;height:32px;padding:0 12px}.dou-dashboard-palette__item:hover:not(:disabled){border-color:var(--dou-color-border-strong)}.dou-dashboard-palette__item:focus-visible{outline:2px solid var(--dou-color-focus);outline-offset:2px}.dou-dashboard-palette__item:disabled{color:var(--dou-color-text-tertiary);cursor:not-allowed}.dou-dashboard-palette__icon{color:var(--dou-color-text-secondary);flex:none;width:16px;height:16px}.dou-dashboard-palette__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dou-dashboard-filter{display:flex;flex-wrap:wrap;align-items: center;gap:8px 16px}.dou-dashboard-filter__field{display:flex;align-items: center;gap:8px;min-width:0}.dou-dashboard-filter__label{color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm);white-space:nowrap;flex:none}.dou-dashboard-filter__control{width:144px;min-width:0}.dou-dashboard-filter__control.is-range{width:272px}.dou-dashboard-filter__extra{display:flex;align-items: center;gap:4px;margin-left:auto}@media (max-width:480px){.dou-dashboard-filter__field{flex:100%}.dou-dashboard-filter__label{width:4em}.dou-dashboard-filter__control,.dou-dashboard-filter__control.is-range{flex:auto;width:auto}}.dou-dashboard-stat{display:flex;container-type:inline-size;flex-direction:column;justify-content:space-between;gap:8px;height:100%}@container (width<=200px){.dou-dashboard-stat .dou-statistic{--dou-statistic-value-size:22px}}.dou-dashboard-stat__spark{flex:none;margin:0 -16px -16px}.dou-dashboard-text{overflow:auto;color:var(--dou-color-text);white-space:pre-wrap;overflow-wrap:anywhere;height:100%;line-height:1.6}.dou-dashboard-text.is-sm{color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm)}.dou-dashboard-text.is-md{font-size:var(--dou-font-size-md)}.dou-dashboard-text.is-lg{font-size:var(--dou-font-size-lg);font-weight:var(--dou-font-weight-medium);line-height:1.4}.dou-dashboard.is-exporting .dou-dashboard__actions{display:none}.dou-dashboard.is-exporting .dou-dashboard__links .dou-tag__close{display:none}.dou-dashboard.is-exporting .dou-dashboard__export-error{display:none}.dou-dashboard.is-exporting .dou-dashboard__peers{display:none}.dou-dashboard.is-exporting .dou-grid-layout__resize{display:none}.dou-dashboard.is-exporting .dou-dashboard__live{display:none}.dou-dashboard.is-exporting .dou-grid-layout__item{outline:none}.dou-dashboard.is-exporting .dou-dashboard__widget{outline:none}@media print{.dou-dashboard{print-color-adjust:exact}.dou-dashboard__actions,.dou-dashboard__links .dou-tag__close,.dou-dashboard__export-error,.dou-dashboard__peers,.dou-grid-layout__resize,.dou-dashboard-filter__clear,.dou-dashboard-filter__extra{display:none}.dou-grid-layout__item{outline:none;break-inside:avoid}.dou-dashboard__widget{outline:none}}}@keyframes dou-dashboard-peer-name{to{opacity:0;visibility:hidden}}
1
+ @layer dou.components{.dou-grid-layout{--dou-grid-item-radius:var(--dou-radius-container);--dou-grid-col:calc((100cqw - (var(--dou-grid-columns) - 1)*var(--dou-grid-gap-x))/var(--dou-grid-columns));position:relative;box-sizing:border-box;height:calc(var(--dou-grid-rows)*var(--dou-grid-row) + max(var(--dou-grid-rows) - 1,0)*var(--dou-grid-gap-y));container-type:inline-size;width:100%;min-width:0}.dou-grid-layout.is-pointer-dragging{cursor:grabbing}.dou-grid-layout.is-resizing{cursor:nwse-resize}.dou-grid-layout__item,.dou-grid-layout__placeholder{position:absolute;box-sizing:border-box;width:calc(var(--dou-grid-w)*var(--dou-grid-col) + (var(--dou-grid-w) - 1)*var(--dou-grid-gap-x) + var(--dou-grid-dw,0px));height:calc(var(--dou-grid-h)*var(--dou-grid-row) + (var(--dou-grid-h) - 1)*var(--dou-grid-gap-y) + var(--dou-grid-dh,0px));border-radius:var(--dou-grid-item-radius);transform:translate(calc(var(--dou-grid-x)*(var(--dou-grid-col) + var(--dou-grid-gap-x)) + var(--dou-grid-dx,0px)),calc(var(--dou-grid-y)*(var(--dou-grid-row) + var(--dou-grid-gap-y)) + var(--dou-grid-dy,0px)));top:0;left:0}.dou-grid-layout__item{display:grid;grid-template:minmax(0,1fr)/minmax(0,1fr);outline:none;min-width:0}.dou-grid-layout__item:focus-visible{outline:2px solid var(--dou-color-focus);outline-offset:2px}.dou-grid-layout.is-editable .dou-grid-layout__item.is-movable{user-select:none;-webkit-touch-callout:none}.dou-grid-layout.is-editable:not(:-webkit-any(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-editable:not(:-moz-any(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-editable:not(:is(.is-pointer-dragging,.has-handle)) .dou-grid-layout__item.is-movable,.dou-grid-layout.is-editable.has-handle:not(.is-pointer-dragging) .dou-grid-layout__item.is-movable .dou-grid-layout__handle{cursor:grab}.dou-grid-layout.is-arranging .dou-grid-layout__item{transition:transform var(--dou-motion-duration-normal)var(--dou-motion-easing)}.dou-grid-layout__item.is-dragging,.dou-grid-layout__item.is-resizing{z-index:2;box-shadow:var(--dou-shadow-lg)}.dou-grid-layout.is-pointer-dragging .dou-grid-layout__item.is-dragging,.dou-grid-layout.is-pointer-dragging .dou-grid-layout__item.is-resizing{transition:none}.dou-grid-layout__placeholder{z-index:1;border:1px dashed var(--dou-color-border-strong);background-color:color-mix(in oklab,var(--dou-color-text)4%,transparent);pointer-events:none}.dou-grid-layout.is-arranging .dou-grid-layout__placeholder{transition:transform var(--dou-motion-duration-fast)var(--dou-motion-easing)}.dou-grid-layout__resize{position:absolute;z-index:1;touch-action:none}.dou-grid-layout__resize.is-e{cursor:ew-resize;width:8px;top:8px;bottom:16px;right:-4px}.dou-grid-layout__resize.is-s{cursor:ns-resize;height:8px;bottom:-4px;left:8px;right:16px}.dou-grid-layout__resize.is-se{cursor:nwse-resize;opacity:0;transition:opacity var(--dou-motion-duration-fast)var(--dou-motion-easing);width:20px;height:20px;bottom:0;right:0}.dou-grid-layout__resize.is-se:after{position:absolute;box-sizing:border-box;border-right:2px solid var(--dou-color-text-tertiary);border-bottom:2px solid var(--dou-color-text-tertiary);content:"";border-bottom-right-radius:4px;width:8px;height:8px;bottom:5px;right:5px}.dou-grid-layout__item:hover>.dou-grid-layout__resize.is-se,.dou-grid-layout__item.is-resizing>.dou-grid-layout__resize.is-se{opacity:1}.dou-grid-layout__item:focus-visible>.dou-grid-layout__resize.is-se{opacity:1}@media (pointer:coarse){.dou-grid-layout__resize.is-se{opacity:1;width:32px;height:32px}}.dou-grid-layout__live{position:absolute;overflow:hidden;clip-path:inset(50%);white-space:nowrap;width:1px;height:1px}}@layer dou.components{.dou-dashboard{position:relative;outline:none;min-width:0}.dou-dashboard__widget{display:flex;box-sizing:border-box;border:1px solid var(--dou-color-border);border-radius:inherit;background-color:var(--dou-color-bg-surface);color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm);overflow:hidden;flex-direction:column;min-width:0;min-height:0}.dou-grid-layout__item.is-dragging>.dou-dashboard__widget,.dou-grid-layout__item.is-resizing>.dou-dashboard__widget{border-color:var(--dou-color-border-strong)}.dou-dashboard__widget.has-peers{outline:1.5px solid var(--dou-dashboard-peer-color);outline-offset:2px}.dou-dashboard__peers{position:absolute;z-index:1;display:flex;overflow:hidden;pointer-events:none;gap:4px;max-width:calc(100% - 24px);margin-bottom:-10px;bottom:100%;left:12px}.dou-dashboard__peer{background-color:var(--dou-dashboard-peer-color);color:oklch(from var(--dou-dashboard-peer-color)clamp(.2,(.66 - l)*100,.99)0 0);font-size:var(--dou-font-size-sm);white-space:nowrap;border-radius:4px;flex:none;padding:0 6px;line-height:18px}.dou-dashboard__peer.is-focus{animation:dou-dashboard-peer-name var(--dou-motion-duration-fast)var(--dou-motion-easing)3s forwards}.dou-dashboard__header{display:flex;flex:none;align-items: center;gap:8px;height:40px;padding:0 8px 0 16px}.dou-dashboard__title{overflow:hidden;color:var(--dou-color-text);font-size:var(--dou-font-size-sm);font-weight:var(--dou-font-weight-medium);text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:min(6em,40%)}.dou-dashboard__title .dou-editable__view{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:inherit}.dou-dashboard__links{display:flex;overflow:hidden;flex:0 auto;gap:4px;min-width:0;max-width:60%}.dou-dashboard__links .dou-tag{min-width:0;max-width:100%}.dou-dashboard__links .dou-tag__label{overflow-x:clip;white-space:nowrap;text-overflow:ellipsis;min-width:0}.dou-dashboard__actions{display:flex;color:var(--dou-color-text-tertiary);flex:none;align-items: center;gap:2px}.dou-dashboard__comment{font-variant-numeric:tabular-nums}.dou-dashboard__comment:not(.has-threads){color:var(--dou-color-text-secondary)}@media (pointer:coarse){.dou-dashboard__comment.has-threads:after{position:absolute;content:"";inset:min(0px,50% - 20px)}}.dou-dashboard__fullscreen{display:flex;flex-direction:column;height:100%;min-height:0}.dou-dashboard__fullscreen>.dou-dashboard__body.is-fullscreen{flex:1;min-height:0}.dou-dashboard__fullscreen-bar{display:flex;justify-content:flex-end;margin-bottom:8px}.dou-dashboard__comment-card{box-sizing:border-box;max-height:min(var(--dou-floating-available,100vh),480px,calc(100vh - 32px));overflow-y:auto;overscroll-behavior:contain;border:1px solid var(--dou-color-border);border-radius:var(--dou-radius-container);background-color:color-mix(in oklab,var(--dou-color-text)calc(4%*var(--dou-scheme-dark)),var(--dou-color-bg-surface));box-shadow:var(--dou-shadow-md);width:min(340px,100vw - 32px);padding:12px}.dou-dashboard__comment-card>.dou-comment-thread+.dou-comment-thread{border-top:1px solid var(--dou-color-border-subtle);margin-top:12px;padding-top:12px}.dou-dashboard__export-error{flex:none;padding:0 8px 8px}.dou-dashboard__export-error .dou-alert__description{overflow-y:auto;max-height:4.5lh}.dou-dashboard__body{position:relative;flex:auto;min-width:0;min-height:0;padding:0 16px 16px}.dou-dashboard__body.is-fullscreen{height:100%;padding:0}.dou-dashboard__content{min-width:0;height:100%}.dou-dashboard__notice{display:grid;color:var(--dou-color-text-tertiary);text-align:center;place-items: center;height:100%}.dou-dashboard__status{position:absolute;display:flex;background-color:var(--dou-color-bg-surface);color:var(--dou-color-text-secondary);text-align:center;flex-direction:column;justify-content:center;align-items: center;gap:8px;padding:0 16px 16px;inset:0}.dou-dashboard__status.is-loading{background-color:color-mix(in oklab,var(--dou-color-bg-surface)72%,transparent);animation:dou-dashboard-fade-in var(--dou-motion-duration-fast)var(--dou-motion-easing).12s both}.dou-dashboard__settings-error{display:flex;color:var(--dou-color-text-secondary);text-align:center;flex-direction:column;align-items: center;gap:8px;padding:24px 0}.dou-dashboard__status-icon{color:var(--dou-color-danger);width:20px;height:20px}.dou-dashboard__empty{position:absolute;display:grid;border:1px dashed var(--dou-color-border);border-radius:var(--dou-radius-container);pointer-events:none;place-items: center;inset:0}.dou-dashboard__live{position:absolute;overflow:hidden;clip-path:inset(50%);white-space:nowrap;width:1px;height:1px}.dou-dashboard__settings{display:flex;flex-direction:column;gap:16px}.dou-dashboard__field{display:flex;flex-direction:column;gap:8px}.dou-dashboard__field-hint{color:var(--dou-color-text-tertiary);font-size:var(--dou-font-size-sm)}.dou-dashboard__field-row{display:flex;flex-wrap:wrap;gap:16px}.dou-dashboard__field-row>.dou-dashboard__field{flex:120px;min-width:0}.dou-dashboard__columns{display:flex;flex-direction:column;gap:8px}.dou-dashboard__column{display:grid;grid-template-columns:minmax(0,1fr)minmax(0,1fr);align-items: center;gap:8px}.dou-dashboard__field-label{color:var(--dou-color-text);font-size:var(--dou-font-size-sm);font-weight:var(--dou-font-weight-medium)}@keyframes dou-dashboard-fade-in{0%{opacity:0}}.dou-dashboard-palette{display:flex;list-style:none;flex-direction:column;gap:8px;margin:0;padding:0}.dou-dashboard-palette.is-horizontal{flex-flow:wrap}.dou-dashboard-palette>li{margin:0}.dou-dashboard-palette__item{display:flex;box-sizing:border-box;border:1px solid var(--dou-color-border);border-radius:var(--dou-radius-control);background-color:var(--dou-color-bg-surface);color:var(--dou-color-text);font:inherit;font-size:var(--dou-font-size-sm);cursor:grab;user-select:none;-webkit-touch-callout:none;transition:border-color var(--dou-motion-duration-fast)var(--dou-motion-easing);align-items: center;gap:8px;width:100%;height:32px;padding:0 12px}.dou-dashboard-palette__item:hover:not(:disabled){border-color:var(--dou-color-border-strong)}.dou-dashboard-palette__item:focus-visible{outline:2px solid var(--dou-color-focus);outline-offset:2px}.dou-dashboard-palette__item:disabled{color:var(--dou-color-text-tertiary);cursor:not-allowed}.dou-dashboard-palette__icon{color:var(--dou-color-text-secondary);flex:none;width:16px;height:16px}.dou-dashboard-palette__label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dou-dashboard-filter{display:flex;flex-wrap:wrap;align-items: center;gap:8px 16px}.dou-dashboard-filter__field{display:flex;align-items: center;gap:8px;min-width:0}.dou-dashboard-filter__label{color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm);white-space:nowrap;flex:none}.dou-dashboard-filter__control{width:144px;min-width:0}.dou-dashboard-filter__control.is-range{width:272px}.dou-dashboard-filter__extra{display:flex;align-items: center;gap:4px;margin-left:auto}@media (max-width:480px){.dou-dashboard-filter__field{flex:100%}.dou-dashboard-filter__label{width:4em}.dou-dashboard-filter__control,.dou-dashboard-filter__control.is-range{flex:auto;width:auto}}.dou-dashboard-stat{display:flex;container-type:inline-size;flex-direction:column;justify-content:space-between;gap:8px;height:100%}@container (width<=200px){.dou-dashboard-stat .dou-statistic{--dou-statistic-value-size:22px}}.dou-dashboard-stat__spark{flex:none;margin:0 -16px -16px}.dou-dashboard-text{overflow:auto;color:var(--dou-color-text);white-space:pre-wrap;overflow-wrap:anywhere;height:100%;line-height:1.6}.dou-dashboard-text.is-sm{color:var(--dou-color-text-secondary);font-size:var(--dou-font-size-sm)}.dou-dashboard-text.is-md{font-size:var(--dou-font-size-md)}.dou-dashboard-text.is-lg{font-size:var(--dou-font-size-lg);font-weight:var(--dou-font-weight-medium);line-height:1.4}.dou-dashboard.is-exporting .dou-dashboard__actions{display:none}.dou-dashboard.is-exporting .dou-dashboard__links .dou-tag__close{display:none}.dou-dashboard.is-exporting .dou-dashboard__export-error{display:none}.dou-dashboard.is-exporting .dou-dashboard__peers{display:none}.dou-dashboard.is-exporting .dou-grid-layout__resize{display:none}.dou-dashboard.is-exporting .dou-dashboard__live{display:none}.dou-dashboard.is-exporting .dou-grid-layout__item{outline:none}.dou-dashboard.is-exporting .dou-dashboard__widget{outline:none}@media print{.dou-dashboard{print-color-adjust:exact}.dou-dashboard__actions,.dou-dashboard__links .dou-tag__close,.dou-dashboard__export-error,.dou-dashboard__peers,.dou-grid-layout__resize,.dou-dashboard-filter__clear,.dou-dashboard-filter__extra{display:none}.dou-grid-layout__item{outline:none;break-inside:avoid}.dou-dashboard__widget{outline:none}}}@keyframes dou-dashboard-peer-name{to{opacity:0;visibility:hidden}}
@@ -0,0 +1,3 @@
1
+ import type { ChartWidgetProps } from './types';
2
+ /** The chart widget's props as its type says; what isn't the right type counts as not set. */
3
+ export declare function chartPropsOf(props: Record<string, unknown>): ChartWidgetProps;
@@ -15,6 +15,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, Das
15
15
  breakpointChange: (breakpoint: string) => any;
16
16
  "update:filters": (value: DashboardFilters) => any;
17
17
  exportError: (event: DashboardExportErrorEvent) => any;
18
+ widgetError: (event: import("./types").DashboardWidgetErrorEvent) => any;
18
19
  "update:comments": (threads: import("dou-ui").CommentThread[]) => any;
19
20
  "update:activeComment": (id: string | null) => any;
20
21
  commentAction: (action: import("dou-ui").CommentAction) => any;
@@ -24,6 +25,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, Das
24
25
  onBreakpointChange?: ((breakpoint: string) => any) | undefined;
25
26
  "onUpdate:filters"?: ((value: DashboardFilters) => any) | undefined;
26
27
  onExportError?: ((event: DashboardExportErrorEvent) => any) | undefined;
28
+ onWidgetError?: ((event: import("./types").DashboardWidgetErrorEvent) => any) | undefined;
27
29
  "onUpdate:comments"?: ((threads: import("dou-ui").CommentThread[]) => any) | undefined;
28
30
  "onUpdate:activeComment"?: ((id: string | null) => any) | undefined;
29
31
  onCommentAction?: ((action: import("dou-ui").CommentAction) => any) | undefined;
@@ -0,0 +1,12 @@
1
+ import type { Ref } from 'vue';
2
+ /**
3
+ * Whether an error broke what a component shows. Errors in event handlers,
4
+ * watchers and the like leave it showing: they go on to the app as usual.
5
+ */
6
+ export declare function breaksView(info: string): boolean;
7
+ /**
8
+ * Keeps an error that breaks a widget's content or settings in that part:
9
+ * `failed` turns on, `report` is told, and the error goes no further. The
10
+ * caller shows the error state and turns `failed` off to try again.
11
+ */
12
+ export declare function useErrorBoundary(label: () => string, report: (error: unknown) => void): Ref<boolean>;
@@ -52,5 +52,7 @@ export interface FrameContext {
52
52
  /** Why the last export from a widget's menu failed. */
53
53
  exportError: (id: string) => string | undefined;
54
54
  clearExportError: (id: string) => void;
55
+ /** A widget's content broke, for `widgetError`. */
56
+ reportError: (id: string, error: unknown) => void;
55
57
  }
56
58
  export declare const frameContextKey: InjectionKey<FrameContext>;
@@ -88,6 +88,17 @@ export interface DashboardSaveOptions<Options = any> extends DashboardExportOpti
88
88
  /** The file's name, with its extension; the widget's title by default. */
89
89
  fileName?: string;
90
90
  }
91
+ /**
92
+ * A widget's component threw while setting up or drawing its content or
93
+ * settings form; that part shows an error with a retry button instead.
94
+ */
95
+ export interface DashboardWidgetErrorEvent {
96
+ /** The widget's id. */
97
+ widget: string;
98
+ /** Which part broke: what the widget shows, or its settings form. */
99
+ part: 'content' | 'settings';
100
+ error: unknown;
101
+ }
91
102
  /** A widget's export from its menu failed. */
92
103
  export interface DashboardExportErrorEvent {
93
104
  /** The widget's id. */
@@ -109,6 +120,12 @@ export interface DashboardEmits {
109
120
  breakpointChange: [breakpoint: string];
110
121
  /** Exporting a widget from its menu or `saveWidget` failed; the widget tells of it unless prevented. */
111
122
  exportError: [event: DashboardExportErrorEvent];
123
+ /**
124
+ * A widget's content or settings form threw while it set up or drew; it
125
+ * shows an error in its place, and the error goes no further. To report
126
+ * to error tracking.
127
+ */
128
+ widgetError: [event: DashboardWidgetErrorEvent];
112
129
  'update:comments': [threads: CommentThread[]];
113
130
  'update:activeComment': [id: string | null];
114
131
  /** A change to the comments made here: to send to a server, notify people, keep a log. */
@@ -13,6 +13,8 @@ type __VLS_Props = {
13
13
  }) => void;
14
14
  /** Where the widget's workbook goes, for its export menu; the full-screen copy has none. */
15
15
  setWorkbook?: (id: string, get: () => SheetWorkbook | undefined) => () => void;
16
+ /** Told when the widget's content breaks, for the dashboard's `widgetError`. */
17
+ reportError?: (id: string, error: unknown) => void;
16
18
  };
17
19
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
18
20
  declare const _default: typeof __VLS_export;
@@ -1,11 +1,15 @@
1
1
  import type { DashboardWidget } from '../core/types';
2
+ import type { DashboardMessages } from '../locale';
2
3
  import type { DashboardWidgetType } from '../widget/define';
3
4
  type __VLS_Props = {
4
5
  widget: DashboardWidget;
5
6
  type: DashboardWidgetType<any>;
7
+ messages: DashboardMessages;
6
8
  setProps: (id: string, patch: Record<string, unknown>, options?: {
7
9
  group?: string;
8
10
  }) => void;
11
+ /** Told when the settings form breaks, for the dashboard's `widgetError`. */
12
+ reportError?: (id: string, error: unknown) => void;
9
13
  };
10
14
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
15
  "update:props": (props: Record<string, unknown>) => any;
@@ -180,6 +180,7 @@ export declare const DouDashboard: import("dou-ui").WithInstall<import("vue").De
180
180
  breakpointChange: (breakpoint: string) => any;
181
181
  "update:filters": (value: import("./index.js").DashboardFilters) => any;
182
182
  exportError: (event: import("./index.js").DashboardExportErrorEvent) => any;
183
+ widgetError: (event: import("./index.js").DashboardWidgetErrorEvent) => any;
183
184
  "update:comments": (threads: import("dou-ui").CommentThread[]) => any;
184
185
  "update:activeComment": (id: string | null) => any;
185
186
  commentAction: (action: import("dou-ui").CommentAction) => any;
@@ -210,6 +211,7 @@ export declare const DouDashboard: import("dou-ui").WithInstall<import("vue").De
210
211
  onBreakpointChange?: ((breakpoint: string) => any) | undefined;
211
212
  "onUpdate:filters"?: ((value: import("./index.js").DashboardFilters) => any) | undefined;
212
213
  onExportError?: ((event: import("./index.js").DashboardExportErrorEvent) => any) | undefined;
214
+ onWidgetError?: ((event: import("./index.js").DashboardWidgetErrorEvent) => any) | undefined;
213
215
  "onUpdate:comments"?: ((threads: import("dou-ui").CommentThread[]) => any) | undefined;
214
216
  "onUpdate:activeComment"?: ((id: string | null) => any) | undefined;
215
217
  onCommentAction?: ((action: import("dou-ui").CommentAction) => any) | undefined;
@@ -282,7 +284,7 @@ export declare const DouDashboardFilterBar: import("dou-ui").WithInstall<{
282
284
  default?: () => unknown;
283
285
  };
284
286
  }), import("vue").HTMLAttributes>;
285
- export type { DashboardAddOptions, DashboardEmits, DashboardExportErrorEvent, DashboardExportOptions, DashboardSaveOptions, DashboardCommentAnchor, DashboardExpose, DashboardFilterBarProps, DashboardPaletteProps, DashboardProps, } from './dashboard/types';
287
+ export type { DashboardAddOptions, DashboardEmits, DashboardExportErrorEvent, DashboardWidgetErrorEvent, DashboardExportOptions, DashboardSaveOptions, DashboardCommentAnchor, DashboardExpose, DashboardFilterBarProps, DashboardPaletteProps, DashboardProps, } from './dashboard/types';
286
288
  export type { DashboardDocument, DashboardPlace, DashboardWidget } from './core/types';
287
289
  export { checkDocument, emptyDocument, isDashboardPlace, migrateDocument } from './core/document';
288
290
  export type { CheckDocumentOptions, DocumentProblem } from './core/document';
@@ -53,6 +53,10 @@ export interface DashboardMessages {
53
53
  /** A widget's status. */
54
54
  loading: string;
55
55
  error: string;
56
+ /** A widget that failed to show, e.g. for props it can't use. */
57
+ renderError: string;
58
+ /** A widget's settings form that failed to show. */
59
+ settingsError: string;
56
60
  empty: string;
57
61
  retry: string;
58
62
  /** A dashboard without widgets, while editing. */
@@ -0,0 +1,3 @@
1
+ import type { PivotWidgetProps } from './types';
2
+ /** The pivot widget's props as its type says; what isn't the right type counts as not set. */
3
+ export declare function pivotPropsOf(props: Record<string, unknown>): PivotWidgetProps;
@@ -1,7 +1,7 @@
1
1
  import type { DashboardWidgetType } from '../widget/define';
2
2
  import type { StatWidgetProps, TableWidgetProps, TextWidgetProps } from './types';
3
3
  export type { StatWidgetProps, TableWidgetColumn, TableWidgetProps, TextWidgetProps } from './types';
4
- export { fieldsOf, filterValueOf, numberFieldsOf, rowsOf, valueAt } from './shared';
4
+ export { fieldsOf, filterValueOf, flagOf, numberFieldsOf, numberOf, recordOf, rowsOf, textOf, textsOf, valueAt, } from './shared';
5
5
  /** What a built-in widget's definition can change: its type, label, icon, size and more. */
6
6
  export type WidgetOverrides<P extends object> = Partial<Omit<DashboardWidgetType<P>, 'component'>>;
7
7
  /**
@@ -0,0 +1,4 @@
1
+ import type { StatWidgetProps, TableWidgetProps, TextWidgetProps } from './types';
2
+ export declare function statPropsOf(props: Record<string, unknown>): StatWidgetProps;
3
+ export declare function textPropsOf(props: Record<string, unknown>): TextWidgetProps;
4
+ export declare function tablePropsOf(props: Record<string, unknown>): TableWidgetProps;
@@ -1,8 +1,24 @@
1
1
  import type { SheetWorkbook } from '@dou-ui/sheet/core';
2
- /** The value at a dotted path, e.g. `total.amount`; the data itself for an empty path. */
3
- export declare function valueAt(data: unknown, path: string | undefined): unknown;
4
- /** The rows of a source's data: the data when it is a list, or the list at `path`. */
5
- export declare function rowsOf(data: unknown, path?: string): Record<string, unknown>[];
2
+ /**
3
+ * The value at a dotted path, e.g. `total.amount`; the data itself for an
4
+ * empty path. A path that isn't text, e.g. a number in JSON written by hand
5
+ * or by a program, finds nothing.
6
+ */
7
+ export declare function valueAt(data: unknown, path: unknown): unknown;
8
+ /** The rows of a source's data: the data when it is a list, or the list at `path`. Items that aren't objects are left out. */
9
+ export declare function rowsOf(data: unknown, path?: unknown): Record<string, unknown>[];
10
+ /** Text, or not set. */
11
+ export declare const textOf: (value: unknown) => string | undefined;
12
+ /** A list of texts: the texts in it, or none. */
13
+ export declare const textsOf: (value: unknown) => string[];
14
+ /** A finite number, or text that is one (`'12'`, as a number read from data); otherwise not set. */
15
+ export declare function numberOf(value: unknown): number | undefined;
16
+ /** On only for `true`: `'no'` or `1` don't turn a switch on. */
17
+ export declare const flagOf: (value: unknown) => boolean;
18
+ /** A plain object, not a list; otherwise not set. */
19
+ export declare const recordOf: (value: unknown) => Record<string, unknown> | undefined;
20
+ /** One of the values listed, or not set. */
21
+ export declare const oneOf: <T extends string>(value: unknown, values: readonly T[]) => T | undefined;
6
22
  /** The keys of the first row of a list, or of an object: the fields to pick from in settings. */
7
23
  export declare function fieldsOf(data: unknown): string[];
8
24
  /** The keys whose value in the first row is a number. */
@@ -1,5 +1,5 @@
1
1
  import { defineDashboardWidget } from "../widget/define.js";
2
- import { fieldsOf, filterValueOf, numberFieldsOf, rowsOf, valueAt } from "./shared.js";
2
+ import { fieldsOf, filterValueOf, flagOf, numberFieldsOf, numberOf, recordOf, rowsOf, textOf, textsOf, valueAt } from "./shared.js";
3
3
  import stat_default from "./stat.js";
4
4
  import stat_settings_default from "./stat-settings.js";
5
5
  import table_default from "./table.js";
@@ -69,6 +69,6 @@ function tableWidget(overrides = {}) {
69
69
  ...overrides
70
70
  });
71
71
  }
72
- export { fieldsOf, filterValueOf, numberFieldsOf, rowsOf, statWidget, tableWidget, textWidget, valueAt };
72
+ export { fieldsOf, filterValueOf, flagOf, numberFieldsOf, numberOf, recordOf, rowsOf, statWidget, tableWidget, textOf, textWidget, textsOf, valueAt };
73
73
 
74
74
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Stat","StatSettings","Text","TextSettings","Table","TableSettings"],"sources":["../../src/widgets/index.ts"],"sourcesContent":["import { HashIcon, TableIcon, TypeIcon } from '@dou-ui/icons'\nimport { defineDashboardWidget } from '../widget/define'\nimport type { DashboardWidgetType } from '../widget/define'\nimport Stat from './stat.vue'\nimport StatSettings from './stat-settings.vue'\nimport Table from './table.vue'\nimport TableSettings from './table-settings.vue'\nimport Text from './text.vue'\nimport TextSettings from './text-settings.vue'\nimport type { StatWidgetProps, TableWidgetProps, TextWidgetProps } from './types'\n\nexport type { StatWidgetProps, TableWidgetColumn, TableWidgetProps, TextWidgetProps } from './types'\nexport { fieldsOf, filterValueOf, numberFieldsOf, rowsOf, valueAt } from './shared'\n\n/** What a built-in widget's definition can change: its type, label, icon, size and more. */\nexport type WidgetOverrides<P extends object> = Partial<Omit<DashboardWidgetType<P>, 'component'>>\n\n/**\n * A number, read from a source or given, with its change since the last\n * period and a sparkline under it.\n */\nexport function statWidget(overrides: WidgetOverrides<StatWidgetProps> = {}) {\n return defineDashboardWidget<StatWidgetProps>({\n type: 'stat',\n label: '统计数字',\n icon: HashIcon,\n size: { w: 3, h: 2, minW: 2, minH: 2 },\n defaultProps: { value: 0 },\n component: Stat,\n settings: StatSettings,\n fullscreen: false,\n ...overrides,\n })\n}\n\n/** A note: plain text in three sizes, its line breaks kept. */\nexport function textWidget(overrides: WidgetOverrides<TextWidgetProps> = {}) {\n return defineDashboardWidget<TextWidgetProps>({\n type: 'text',\n label: '文字',\n icon: TypeIcon,\n size: { w: 4, h: 2, minW: 2, minH: 1 },\n defaultProps: { text: '', size: 'md' },\n component: Text,\n settings: TextSettings,\n fullscreen: false,\n ...overrides,\n })\n}\n\n/** A source's rows in a table, with the columns picked in its settings. */\nexport function tableWidget(overrides: WidgetOverrides<TableWidgetProps> = {}) {\n return defineDashboardWidget<TableWidgetProps>({\n type: 'table',\n label: '表格',\n icon: TableIcon,\n size: { w: 6, h: 4, minW: 3, minH: 3 },\n component: Table,\n settings: TableSettings,\n ...overrides,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAqBA,SAAgB,WAAW,YAA8C,CAAC,GAAG;CAC3E,OAAO,sBAAuC;EAC5C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,cAAc,EAAE,OAAO,EAAE;EACzB,WAAWA;EACX,UAAUC;EACV,YAAY;EACZ,GAAG;CACL,CAAC;AACH;;AAGA,SAAgB,WAAW,YAA8C,CAAC,GAAG;CAC3E,OAAO,sBAAuC;EAC5C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,cAAc;GAAE,MAAM;GAAI,MAAM;EAAK;EACrC,WAAWC;EACX,UAAUC;EACV,YAAY;EACZ,GAAG;CACL,CAAC;AACH;;AAGA,SAAgB,YAAY,YAA+C,CAAC,GAAG;CAC7E,OAAO,sBAAwC;EAC7C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,WAAWC;EACX,UAAUC;EACV,GAAG;CACL,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":["Stat","StatSettings","Text","TextSettings","Table","TableSettings"],"sources":["../../src/widgets/index.ts"],"sourcesContent":["import { HashIcon, TableIcon, TypeIcon } from '@dou-ui/icons'\nimport { defineDashboardWidget } from '../widget/define'\nimport type { DashboardWidgetType } from '../widget/define'\nimport Stat from './stat.vue'\nimport StatSettings from './stat-settings.vue'\nimport Table from './table.vue'\nimport TableSettings from './table-settings.vue'\nimport Text from './text.vue'\nimport TextSettings from './text-settings.vue'\nimport type { StatWidgetProps, TableWidgetProps, TextWidgetProps } from './types'\n\nexport type { StatWidgetProps, TableWidgetColumn, TableWidgetProps, TextWidgetProps } from './types'\nexport {\n fieldsOf,\n filterValueOf,\n flagOf,\n numberFieldsOf,\n numberOf,\n recordOf,\n rowsOf,\n textOf,\n textsOf,\n valueAt,\n} from './shared'\n\n/** What a built-in widget's definition can change: its type, label, icon, size and more. */\nexport type WidgetOverrides<P extends object> = Partial<Omit<DashboardWidgetType<P>, 'component'>>\n\n/**\n * A number, read from a source or given, with its change since the last\n * period and a sparkline under it.\n */\nexport function statWidget(overrides: WidgetOverrides<StatWidgetProps> = {}) {\n return defineDashboardWidget<StatWidgetProps>({\n type: 'stat',\n label: '统计数字',\n icon: HashIcon,\n size: { w: 3, h: 2, minW: 2, minH: 2 },\n defaultProps: { value: 0 },\n component: Stat,\n settings: StatSettings,\n fullscreen: false,\n ...overrides,\n })\n}\n\n/** A note: plain text in three sizes, its line breaks kept. */\nexport function textWidget(overrides: WidgetOverrides<TextWidgetProps> = {}) {\n return defineDashboardWidget<TextWidgetProps>({\n type: 'text',\n label: '文字',\n icon: TypeIcon,\n size: { w: 4, h: 2, minW: 2, minH: 1 },\n defaultProps: { text: '', size: 'md' },\n component: Text,\n settings: TextSettings,\n fullscreen: false,\n ...overrides,\n })\n}\n\n/** A source's rows in a table, with the columns picked in its settings. */\nexport function tableWidget(overrides: WidgetOverrides<TableWidgetProps> = {}) {\n return defineDashboardWidget<TableWidgetProps>({\n type: 'table',\n label: '表格',\n icon: TableIcon,\n size: { w: 6, h: 4, minW: 3, minH: 3 },\n component: Table,\n settings: TableSettings,\n ...overrides,\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAgCA,SAAgB,WAAW,YAA8C,CAAC,GAAG;CAC3E,OAAO,sBAAuC;EAC5C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,cAAc,EAAE,OAAO,EAAE;EACzB,WAAWA;EACX,UAAUC;EACV,YAAY;EACZ,GAAG;CACL,CAAC;AACH;;AAGA,SAAgB,WAAW,YAA8C,CAAC,GAAG;CAC3E,OAAO,sBAAuC;EAC5C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,cAAc;GAAE,MAAM;GAAI,MAAM;EAAK;EACrC,WAAWC;EACX,UAAUC;EACV,YAAY;EACZ,GAAG;CACL,CAAC;AACH;;AAGA,SAAgB,YAAY,YAA+C,CAAC,GAAG;CAC7E,OAAO,sBAAwC;EAC7C,MAAM;EACN,OAAO;EACP,MAAM;EACN,MAAM;GAAE,GAAG;GAAG,GAAG;GAAG,MAAM;GAAG,MAAM;EAAE;EACrC,WAAWC;EACX,UAAUC;EACV,GAAG;CACL,CAAC;AACH"}
@@ -0,0 +1,52 @@
1
+ import { numberOf, oneOf, recordOf, textOf } from "./shared.js";
2
+ // src/widgets/read-props.ts
3
+ function statPropsOf(props) {
4
+ const precision = numberOf(props.precision);
5
+ return {
6
+ source: textOf(props.source),
7
+ field: textOf(props.field),
8
+ value: numberOf(props.value),
9
+ prefix: textOf(props.prefix),
10
+ suffix: textOf(props.suffix),
11
+ precision: precision !== void 0 && Number.isInteger(precision) && precision >= 0 && precision <= 20 ? precision : void 0,
12
+ trend: textOf(props.trend),
13
+ delta: textOf(props.delta)
14
+ };
15
+ }
16
+ function textPropsOf(props) {
17
+ const text = props.text;
18
+ return {
19
+ text: typeof text === "number" ? String(text) : textOf(text),
20
+ size: oneOf(props.size, [
21
+ "sm",
22
+ "md",
23
+ "lg"
24
+ ])
25
+ };
26
+ }
27
+ /** The columns given, those with a text `key`; not set when not a list. */
28
+ function columnsOf(value) {
29
+ if (!Array.isArray(value)) return void 0;
30
+ const columns = [];
31
+ for (const item of value) {
32
+ const column = recordOf(item);
33
+ if (typeof column?.key !== "string") continue;
34
+ const title = textOf(column.title);
35
+ columns.push(title === void 0 ? { key: column.key } : {
36
+ key: column.key,
37
+ title
38
+ });
39
+ }
40
+ return columns;
41
+ }
42
+ function tablePropsOf(props) {
43
+ return {
44
+ source: textOf(props.source),
45
+ path: textOf(props.path),
46
+ columns: columnsOf(props.columns),
47
+ rowKey: textOf(props.rowKey)
48
+ };
49
+ }
50
+ export { statPropsOf, tablePropsOf, textPropsOf };
51
+
52
+ //# sourceMappingURL=read-props.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read-props.js","names":[],"sources":["../../src/widgets/read-props.ts"],"sourcesContent":["import { numberOf, oneOf, recordOf, textOf } from './shared'\nimport type { StatWidgetProps, TableWidgetColumn, TableWidgetProps, TextWidgetProps } from './types'\n\n/*\n * The props of the built-in widgets as their types say, from whatever JSON\n * was saved: what isn't the right type counts as not set. The widgets and\n * their settings both read through these.\n */\n\nexport function statPropsOf(props: Record<string, unknown>): StatWidgetProps {\n const precision = numberOf(props.precision)\n return {\n source: textOf(props.source),\n field: textOf(props.field),\n value: numberOf(props.value),\n prefix: textOf(props.prefix),\n suffix: textOf(props.suffix),\n precision:\n precision !== undefined && Number.isInteger(precision) && precision >= 0 && precision <= 20\n ? precision\n : undefined,\n trend: textOf(props.trend),\n delta: textOf(props.delta),\n }\n}\n\nexport function textPropsOf(props: Record<string, unknown>): TextWidgetProps {\n const text = props.text\n return {\n text: typeof text === 'number' ? String(text) : textOf(text),\n size: oneOf(props.size, ['sm', 'md', 'lg'] as const),\n }\n}\n\n/** The columns given, those with a text `key`; not set when not a list. */\nfunction columnsOf(value: unknown): TableWidgetColumn[] | undefined {\n if (!Array.isArray(value)) return undefined\n const columns: TableWidgetColumn[] = []\n for (const item of value) {\n const column = recordOf(item)\n if (typeof column?.key !== 'string') continue\n const title = textOf(column.title)\n columns.push(title === undefined ? { key: column.key } : { key: column.key, title })\n }\n return columns\n}\n\nexport function tablePropsOf(props: Record<string, unknown>): TableWidgetProps {\n return {\n source: textOf(props.source),\n path: textOf(props.path),\n columns: columnsOf(props.columns),\n rowKey: textOf(props.rowKey),\n }\n}\n"],"mappings":";;AASA,SAAgB,YAAY,OAAiD;CAC3E,MAAM,YAAY,SAAS,MAAM,SAAS;CAC1C,OAAO;EACL,QAAQ,OAAO,MAAM,MAAM;EAC3B,OAAO,OAAO,MAAM,KAAK;EACzB,OAAO,SAAS,MAAM,KAAK;EAC3B,QAAQ,OAAO,MAAM,MAAM;EAC3B,QAAQ,OAAO,MAAM,MAAM;EAC3B,WACE,cAAc,KAAA,KAAa,OAAO,UAAU,SAAS,KAAK,aAAa,KAAK,aAAa,KACrF,YACA,KAAA;EACN,OAAO,OAAO,MAAM,KAAK;EACzB,OAAO,OAAO,MAAM,KAAK;CAC3B;AACF;AAEA,SAAgB,YAAY,OAAiD;CAC3E,MAAM,OAAO,MAAM;CACnB,OAAO;EACL,MAAM,OAAO,SAAS,WAAW,OAAO,IAAI,IAAI,OAAO,IAAI;EAC3D,MAAM,MAAM,MAAM,MAAM;GAAC;GAAM;GAAM;EAAI,CAAU;CACrD;AACF;;AAGA,SAAS,UAAU,OAAiD;CAClE,IAAI,CAAC,MAAM,QAAQ,KAAK,GAAG,OAAO,KAAA;CAClC,MAAM,UAA+B,CAAC;CACtC,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,SAAS,IAAI;EAC5B,IAAI,OAAO,QAAQ,QAAQ,UAAU;EACrC,MAAM,QAAQ,OAAO,OAAO,KAAK;EACjC,QAAQ,KAAK,UAAU,KAAA,IAAY,EAAE,KAAK,OAAO,IAAI,IAAI;GAAE,KAAK,OAAO;GAAK;EAAM,CAAC;CACrF;CACA,OAAO;AACT;AAEA,SAAgB,aAAa,OAAkD;CAC7E,OAAO;EACL,QAAQ,OAAO,MAAM,MAAM;EAC3B,MAAM,OAAO,MAAM,IAAI;EACvB,SAAS,UAAU,MAAM,OAAO;EAChC,QAAQ,OAAO,MAAM,MAAM;CAC7B;AACF"}
@@ -1,7 +1,12 @@
1
1
  // src/widgets/shared.ts
2
- /** The value at a dotted path, e.g. `total.amount`; the data itself for an empty path. */
2
+ /**
3
+ * The value at a dotted path, e.g. `total.amount`; the data itself for an
4
+ * empty path. A path that isn't text, e.g. a number in JSON written by hand
5
+ * or by a program, finds nothing.
6
+ */
3
7
  function valueAt(data, path) {
4
- if (!path) return data;
8
+ if (path === void 0 || path === null || path === "") return data;
9
+ if (typeof path !== "string") return void 0;
5
10
  let value = data;
6
11
  for (const key of path.split(".")) {
7
12
  if (value == null || typeof value !== "object") return void 0;
@@ -9,11 +14,28 @@ function valueAt(data, path) {
9
14
  }
10
15
  return value;
11
16
  }
12
- /** The rows of a source's data: the data when it is a list, or the list at `path`. */
17
+ /** The rows of a source's data: the data when it is a list, or the list at `path`. Items that aren't objects are left out. */
13
18
  function rowsOf(data, path) {
14
19
  const value = valueAt(data, path);
15
- return Array.isArray(value) ? value : [];
20
+ if (!Array.isArray(value)) return [];
21
+ return value.every(isRecord) ? value : value.filter(isRecord);
22
+ }
23
+ var isRecord = (value) => value !== null && typeof value === "object";
24
+ /** Text, or not set. */
25
+ var textOf = (value) => typeof value === "string" ? value : void 0;
26
+ /** A list of texts: the texts in it, or none. */
27
+ var textsOf = (value) => Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
28
+ /** A finite number, or text that is one (`'12'`, as a number read from data); otherwise not set. */
29
+ function numberOf(value) {
30
+ const n = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : NaN;
31
+ return Number.isFinite(n) ? n : void 0;
16
32
  }
33
+ /** On only for `true`: `'no'` or `1` don't turn a switch on. */
34
+ var flagOf = (value) => value === true;
35
+ /** A plain object, not a list; otherwise not set. */
36
+ var recordOf = (value) => isRecord(value) && !Array.isArray(value) ? value : void 0;
37
+ /** One of the values listed, or not set. */
38
+ var oneOf = (value, values) => values.includes(value) ? value : void 0;
17
39
  /** The keys of the first row of a list, or of an object: the fields to pick from in settings. */
18
40
  function fieldsOf(data) {
19
41
  const first = Array.isArray(data) ? data[0] : data;
@@ -63,6 +85,6 @@ function rowsToWorkbook(name, columns, rows) {
63
85
  styles: { head: { bold: true } }
64
86
  };
65
87
  }
66
- export { fieldsOf, filterValueOf, numberFieldsOf, rowsOf, rowsToWorkbook, valueAt };
88
+ export { fieldsOf, filterValueOf, flagOf, numberFieldsOf, numberOf, oneOf, recordOf, rowsOf, rowsToWorkbook, textOf, textsOf, valueAt };
67
89
 
68
90
  //# sourceMappingURL=shared.js.map