@graphenedata/cli 0.0.14 → 0.0.16

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 (121) hide show
  1. package/LICENSE.md +3 -3
  2. package/README.md +138 -0
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/bin.js +2 -2
  5. package/dist/cli/bigQuery-I3F46SC6.js +75 -0
  6. package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
  7. package/dist/cli/chunk-OVWODUTJ.js +12849 -0
  8. package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
  9. package/dist/cli/chunk-QAXEOZ43.js +53 -0
  10. package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
  11. package/dist/cli/cli.js +245 -10290
  12. package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
  13. package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
  14. package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
  15. package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
  16. package/dist/cli/serve2-TNN5EROW.js +447 -0
  17. package/dist/cli/serve2-TNN5EROW.js.map +7 -0
  18. package/dist/cli/snowflake-MOQB5GA4.js +128 -0
  19. package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
  20. package/dist/index.d.ts +63 -0
  21. package/dist/lang/index.d.ts +63 -0
  22. package/dist/skills/graphene/SKILL.md +235 -0
  23. package/dist/skills/graphene/references/big-value.md +20 -0
  24. package/dist/skills/graphene/references/date-range.md +64 -0
  25. package/dist/skills/graphene/references/dropdown.md +62 -0
  26. package/dist/skills/graphene/references/echarts.md +162 -0
  27. package/dist/skills/graphene/references/gsql.md +393 -0
  28. package/dist/skills/graphene/references/model-gsql.md +72 -0
  29. package/dist/skills/graphene/references/table.md +143 -0
  30. package/dist/skills/graphene/references/text-input.md +29 -0
  31. package/dist/ui/app.css +263 -299
  32. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  33. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  34. package/dist/ui/component-utilities/enrich.ts +763 -0
  35. package/dist/ui/component-utilities/format.ts +177 -0
  36. package/dist/ui/component-utilities/inputUtils.ts +48 -9
  37. package/dist/ui/component-utilities/theme.ts +200 -0
  38. package/dist/ui/component-utilities/themeStores.ts +26 -21
  39. package/dist/ui/component-utilities/types.ts +70 -0
  40. package/dist/ui/components/AreaChart.svelte +57 -105
  41. package/dist/ui/components/BarChart.svelte +71 -129
  42. package/dist/ui/components/BigValue.svelte +24 -40
  43. package/dist/ui/components/Column.svelte +11 -19
  44. package/dist/ui/components/DateRange.svelte +71 -34
  45. package/dist/ui/components/Dropdown.svelte +82 -49
  46. package/dist/ui/components/DropdownOption.svelte +1 -2
  47. package/dist/ui/components/ECharts.svelte +179 -60
  48. package/dist/ui/components/InlineDelta.svelte +51 -32
  49. package/dist/ui/components/LineChart.svelte +54 -125
  50. package/dist/ui/components/PieChart.svelte +27 -37
  51. package/dist/ui/components/QueryLoad.svelte +78 -44
  52. package/dist/ui/components/Row.svelte +2 -1
  53. package/dist/ui/components/ScatterPlot.svelte +52 -0
  54. package/dist/ui/components/Skeleton.svelte +32 -0
  55. package/dist/ui/components/Table.svelte +3 -2
  56. package/dist/ui/components/TableGroupRow.svelte +28 -36
  57. package/dist/ui/components/TableHarness.svelte +32 -0
  58. package/dist/ui/components/TableHeader.svelte +34 -59
  59. package/dist/ui/components/TableRow.svelte +15 -39
  60. package/dist/ui/components/TableSubtotalRow.svelte +26 -21
  61. package/dist/ui/components/TableTotalRow.svelte +27 -37
  62. package/dist/ui/components/TextInput.svelte +17 -14
  63. package/dist/ui/components/Value.svelte +25 -0
  64. package/dist/ui/components/_Table.svelte +80 -76
  65. package/dist/ui/internal/ChartGallery.svelte +527 -0
  66. package/dist/ui/internal/ErrorDisplay.svelte +60 -0
  67. package/dist/ui/internal/LocalApp.svelte +87 -19
  68. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  69. package/dist/ui/internal/Sidebar.svelte +178 -0
  70. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  71. package/dist/ui/internal/StyleGallery.svelte +244 -0
  72. package/dist/ui/internal/clientCache.ts +15 -13
  73. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  74. package/dist/ui/internal/queryEngine.ts +124 -132
  75. package/dist/ui/internal/runSocket.ts +59 -0
  76. package/dist/ui/internal/sidebar.svelte.js +18 -0
  77. package/dist/ui/internal/telemetry.ts +52 -17
  78. package/dist/ui/internal/types.d.ts +7 -0
  79. package/dist/ui/web.js +55 -13
  80. package/package.json +40 -41
  81. package/dist/docs/agent-instructions.md +0 -18
  82. package/dist/docs/base.md +0 -98
  83. package/dist/docs/cli.md +0 -22
  84. package/dist/docs/graphene.md +0 -1462
  85. package/dist/ui/component-utilities/autoFormatting.js +0 -301
  86. package/dist/ui/component-utilities/builtInFormats.js +0 -482
  87. package/dist/ui/component-utilities/chartContext.js +0 -12
  88. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  89. package/dist/ui/component-utilities/checkInputs.js +0 -95
  90. package/dist/ui/component-utilities/convert.js +0 -15
  91. package/dist/ui/component-utilities/dateParsing.js +0 -57
  92. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  93. package/dist/ui/component-utilities/echarts.js +0 -272
  94. package/dist/ui/component-utilities/echartsThemes.js +0 -453
  95. package/dist/ui/component-utilities/formatTitle.js +0 -24
  96. package/dist/ui/component-utilities/formatting.js +0 -250
  97. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  98. package/dist/ui/component-utilities/getColumnSummary.js +0 -67
  99. package/dist/ui/component-utilities/getCompletedData.js +0 -114
  100. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  101. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  102. package/dist/ui/component-utilities/getSeriesConfig.js +0 -237
  103. package/dist/ui/component-utilities/getSortedData.js +0 -7
  104. package/dist/ui/component-utilities/getStackPercentages.js +0 -43
  105. package/dist/ui/component-utilities/getStackedData.js +0 -17
  106. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  107. package/dist/ui/component-utilities/globalContexts.js +0 -1
  108. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  109. package/dist/ui/component-utilities/replaceNulls.js +0 -14
  110. package/dist/ui/component-utilities/tableUtils.ts +0 -120
  111. package/dist/ui/components/Area.svelte +0 -214
  112. package/dist/ui/components/Bar.svelte +0 -350
  113. package/dist/ui/components/Chart.svelte +0 -989
  114. package/dist/ui/components/ErrorChart.svelte +0 -118
  115. package/dist/ui/components/Line.svelte +0 -227
  116. package/dist/ui/internal/NavSidebar.svelte +0 -396
  117. package/dist/ui/internal/PageError.svelte +0 -23
  118. package/dist/ui/internal/checkSocket.ts +0 -48
  119. package/dist/ui/internal/theme.ts +0 -88
  120. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  121. package/dist/ui/public/inter-latin.woff2 +0 -0
@@ -1,30 +1,40 @@
1
1
  <script lang="ts">
2
- import {onDestroy, onMount, type Snippet} from 'svelte'
3
- import ErrorChart from './ErrorChart.svelte'
2
+ import {onDestroy, onMount, untrack, type Snippet} from 'svelte'
3
+ import type {GrapheneError} from '../../lang/index.d.ts'
4
+ import ErrorDisplay from '../internal/ErrorDisplay.svelte'
5
+ import type {QueryResult} from '../component-utilities/types.ts'
6
+ import {componentLogger} from '../internal/telemetry.ts'
7
+ import Skeleton from './Skeleton.svelte'
4
8
 
5
9
  interface Props {
6
- data: string | {rows?: any[]}
10
+ data: string | QueryResult
7
11
  height?: number
8
12
  fields?: Record<string, string | string[]>
9
- children?: Snippet<[any[]]>
13
+ inline?: boolean
14
+ componentId?: string
15
+ children?: Snippet<[QueryResult]>
10
16
  }
11
17
 
12
- let {data, height = 200, fields = {}, children}: Props = $props()
18
+ let {data, height = 200, fields = {}, inline = false, componentId = undefined, children}: Props = $props()
19
+ let logger = untrack(() => componentLogger(componentId || 'QueryLoad', componentId ? {} : {data: typeof data == 'string' ? data : undefined, ...fields}))
13
20
 
14
- let errors: Error[] | null = $state(null)
15
- let loaded: any[] | null = $state(null)
21
+ let error: GrapheneError | null = $state(null)
22
+ let loaded: QueryResult | null = $state(null)
23
+ let tooltipId = `query-error-${Math.random().toString(36).slice(2)}`
16
24
 
17
- let handleResults = (result: any) => {
18
- errors = result.errors || null
19
- loaded = result.rows
25
+ let handleResults = (result: QueryResult) => {
26
+ error = result?.error || null
27
+ loaded = {rows: result?.rows ?? [], fields: result?.fields ?? [], error: result?.error, sql: result?.sql}
28
+ if (result?.error) logger.error(result.error, {...result.error, componentId: logger.id})
20
29
  }
21
30
 
22
31
  onMount(() => {
23
32
  if (typeof data !== 'string') {
24
- loaded = data.rows ?? null
33
+ error = data.error || null
34
+ loaded = {rows: data.rows ?? [], fields: data.fields ?? [], error: data.error, sql: data.sql}
25
35
  } else {
26
36
  let usedFields = Object.fromEntries(Object.entries(fields).filter(e => !!e[1]))
27
- window.$GRAPHENE.query(data, usedFields, handleResults)
37
+ window.$GRAPHENE.query(data, usedFields, handleResults, logger.id)
28
38
  }
29
39
  })
30
40
 
@@ -33,45 +43,28 @@
33
43
  })
34
44
  </script>
35
45
 
36
- {#if errors}
37
- <ErrorChart title="Error" error={errors[0]} />
46
+ {#if error}
47
+ {#if inline}
48
+ <span class="inline-error">
49
+ <button class="inline-error__icon" type="button" aria-label="Query failed" aria-describedby={tooltipId}>!</button>
50
+ <span class="inline-error__tooltip" id={tooltipId} role="tooltip">
51
+ <ErrorDisplay {error} />
52
+ </span>
53
+ </span>
54
+ {:else}
55
+ <div style="min-height:{height}px;width:100%;display:grid;align-content:center;padding:8px;box-sizing:border-box">
56
+ <ErrorDisplay {error} />
57
+ </div>
58
+ {/if}
38
59
  {:else if !loaded}
39
- <div class='ql-skeleton' style={`height:${height}px`} role="status" aria-live="polite">
40
- <span class="ql-skeleton__pulse"></span>
41
- </div>
42
- {:else if loaded.length == 0}
60
+ <Skeleton />
61
+ {:else if loaded.rows.length == 0}
43
62
  <div class="empty-chart" role="note">Dataset is empty - query ran successfully, but no data was returned from the database</div>
44
63
  {:else}
45
64
  {@render children?.(loaded)}
46
65
  {/if}
47
66
 
48
67
  <style>
49
- .ql-skeleton {
50
- width: 100%;
51
- position: relative;
52
- overflow: hidden;
53
- background: var(--chart-skeleton-bg, #f3f4f6);
54
- border-radius: 4px;
55
- }
56
-
57
- .ql-skeleton__pulse {
58
- position: absolute;
59
- inset: 0;
60
- transform: translateX(-100%);
61
- background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.55) 50%, rgba(255, 255, 255, 0) 100%);
62
- animation: ql-pulse 1.4s ease-in-out infinite;
63
- content: '';
64
- }
65
-
66
- @keyframes ql-pulse {
67
- 0% {
68
- transform: translateX(-100%);
69
- }
70
- 100% {
71
- transform: translateX(100%);
72
- }
73
- }
74
-
75
68
  .empty-chart {
76
69
  width: 100%;
77
70
  padding: 12px;
@@ -83,4 +76,45 @@
83
76
  text-align: center;
84
77
  background: rgba(243, 244, 246, 0.6);
85
78
  }
79
+
80
+ .inline-error {
81
+ position: relative;
82
+ display: inline-flex;
83
+ align-items: center;
84
+ vertical-align: middle;
85
+ }
86
+
87
+ .inline-error__icon {
88
+ width: 1.05em;
89
+ height: 1.05em;
90
+ padding: 0;
91
+ display: inline-flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ border: 1px solid var(--graphene-error-border, #ef4444);
95
+ border-radius: 999px;
96
+ background: var(--graphene-error-background, #fef2f2);
97
+ color: var(--graphene-error-content-strong, #b91c1c);
98
+ cursor: help;
99
+ font: inherit;
100
+ font-size: 0.75em;
101
+ font-weight: 700;
102
+ line-height: 1;
103
+ }
104
+
105
+ .inline-error__tooltip {
106
+ display: none;
107
+ position: absolute;
108
+ z-index: 1000;
109
+ top: calc(100% + 8px);
110
+ left: 0;
111
+ width: min(420px, 80vw);
112
+ text-align: left;
113
+ filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.18));
114
+ }
115
+
116
+ .inline-error:hover .inline-error__tooltip,
117
+ .inline-error:focus-within .inline-error__tooltip {
118
+ display: block;
119
+ }
86
120
  </style>
@@ -4,11 +4,12 @@
4
4
  div {
5
5
  display: flex;
6
6
  flex-direction: row;
7
- gap: 1rem;
7
+ gap: 1.5rem;
8
8
  }
9
9
 
10
10
  /* Ensure styles apply to slotted children (not scoped) */
11
11
  div > :global(*) {
12
12
  flex: 1 1 0;
13
+ min-width: 0; /* avoids some rendering instability */
13
14
  }
14
15
  </style>
@@ -0,0 +1,52 @@
1
+ <script lang="ts">
2
+ import ECharts from './ECharts.svelte'
3
+ import {parseCommaList} from '../component-utilities/inputUtils.ts'
4
+ import type {EChartsConfig, QueryResult, SeriesWithGroupingHint} from '../component-utilities/types.ts'
5
+
6
+ interface Props {
7
+ data: string | QueryResult
8
+ x: string
9
+ y: string
10
+ splitBy?: string
11
+ title?: string
12
+ height?: string | number
13
+ width?: string | number
14
+ }
15
+
16
+ let {
17
+ data,
18
+ x,
19
+ y,
20
+ splitBy = undefined,
21
+ title = undefined,
22
+ height = undefined,
23
+ width = undefined,
24
+ }: Props = $props()
25
+
26
+ function buildConfig(): EChartsConfig {
27
+ let yFields = parseCommaList(y)
28
+ if (splitBy && yFields.length > 1) throw new Error('ScatterPlot does not support splitBy with multiple y fields')
29
+
30
+ let series: SeriesWithGroupingHint[]
31
+
32
+ if (splitBy) {
33
+ // "tall" data, one template split into one series per splitBy value by enrich()
34
+ series = [{type: 'scatter' as const, encode: {x, y: yFields[0], splitBy}}]
35
+ } else {
36
+ // "wide" data, one scatter series per field listed in y
37
+ series = yFields.map(field => ({type: 'scatter' as const, name: field, encode: {x, y: field}}))
38
+ }
39
+
40
+ return {
41
+ title: title ? {text: title} : undefined,
42
+ tooltip: {trigger: 'item'},
43
+ legend: {show: Boolean(splitBy || yFields.length > 1)},
44
+ grid: {left: 56, bottom: 52},
45
+ xAxis: {name: x, nameLocation: 'middle', nameGap: 28},
46
+ yAxis: {name: y, nameLocation: 'middle', nameGap: 40},
47
+ series,
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <ECharts data={data} config={buildConfig()} {height} {width} />
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ let {height}: {height: number} = $props()
3
+ </script>
4
+
5
+ <div class='ql-skeleton' style={`height:${height}px`} role="status" aria-live="polite">
6
+ <span class="ql-skeleton__pulse"></span>
7
+ </div>
8
+
9
+ <style>
10
+ .ql-skeleton {
11
+ width: 100%;
12
+ height: 100%;
13
+ position: relative;
14
+ overflow: hidden;
15
+ background: var(--chart-skeleton-bg, #f3f4f6);
16
+ border-radius: 4px;
17
+ }
18
+
19
+ .ql-skeleton__pulse {
20
+ position: absolute;
21
+ inset: 0;
22
+ transform: translateX(-100%);
23
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.55) 50%, rgba(255, 255, 255, 0) 100%);
24
+ animation: ql-pulse 1.4s ease-in-out infinite;
25
+ content: '';
26
+ }
27
+
28
+ @keyframes ql-pulse {
29
+ 0% { transform: translateX(-100%); }
30
+ 100% { transform: translateX(100%); }
31
+ }
32
+ </style>
@@ -1,10 +1,11 @@
1
1
  <script lang="ts">
2
2
  import type {Snippet} from 'svelte'
3
+ import type {QueryResult} from '../component-utilities/types.ts'
3
4
  import QueryLoad from './QueryLoad.svelte'
4
5
  import TableInner from './_Table.svelte'
5
6
 
6
7
  interface Props {
7
- data: string
8
+ data: string | QueryResult
8
9
  children?: Snippet
9
10
  [key: string]: unknown
10
11
  }
@@ -14,7 +15,7 @@
14
15
  let spreadProps = $derived(Object.fromEntries(Object.entries(restProps).filter(([, value]) => value !== undefined)))
15
16
  </script>
16
17
 
17
- {#snippet tableContent(loaded: any[])}
18
+ {#snippet tableContent(loaded: QueryResult)}
18
19
  {#if children}
19
20
  <TableInner {...spreadProps} data={loaded} {children} />
20
21
  {:else}
@@ -2,15 +2,14 @@
2
2
  import TableCell from './TableCell.svelte'
3
3
  import TableGroupToggle from './TableGroupToggle.svelte'
4
4
  import InlineDelta from './InlineDelta.svelte'
5
- import {aggregateColumn, safeExtractColumn} from '../component-utilities/tableUtils'
6
- import {formatValue, getFormatObjectFromString} from '../component-utilities/formatting.js'
7
- import {toBoolean} from '../component-utilities/convert'
5
+ import {summarizeColumn, type SummaryMetric} from '../component-utilities/dataSummary.ts'
6
+ import {formatFromField} from '../component-utilities/format.ts'
7
+ import {toBoolean} from '../component-utilities/inputUtils'
8
8
 
9
9
  interface Props {
10
10
  groupName: string
11
11
  currentGroupData?: any[]
12
12
  toggled?: boolean
13
- columnSummary?: any[]
14
13
  rowNumbers?: boolean | string
15
14
  rowColor?: string
16
15
  subtotals?: boolean | string
@@ -20,7 +19,7 @@
20
19
  }
21
20
 
22
21
  let {
23
- groupName, currentGroupData = [], toggled = true, columnSummary = [],
22
+ groupName, currentGroupData = [], toggled = true,
24
23
  rowNumbers: rowNumbersProp = undefined, rowColor = undefined, subtotals: subtotalsProp = true,
25
24
  orderedColumns = [], compact: compactProp = undefined, onToggle,
26
25
  }: Props = $props()
@@ -29,6 +28,16 @@
29
28
  let subtotals = $derived(toBoolean(subtotalsProp) ?? true)
30
29
  let compact = $derived(toBoolean(compactProp))
31
30
 
31
+ const SUPPORTED_METRICS: SummaryMetric[] = ['sum', 'mean', 'median', 'min', 'max', 'count', 'countDistinct']
32
+
33
+ const getAggregateValue = (rows: Record<string, unknown>[], column: any) => {
34
+ let metric = column?.totalAgg as SummaryMetric | undefined
35
+ if (!metric && String(column?.type || '').toLowerCase() === 'number') metric = 'sum'
36
+ if (!metric || !SUPPORTED_METRICS.includes(metric)) return '-'
37
+ let summary = summarizeColumn(rows, column.field ?? {name: column.id, type: column.type}, [metric])
38
+ return summary[metric] ?? null
39
+ }
40
+
32
41
  const toggleGroup = () => onToggle?.({groupName})
33
42
 
34
43
  const handleKeydown = (event: KeyboardEvent) => {
@@ -38,12 +47,6 @@
38
47
  }
39
48
  }
40
49
 
41
- const resolveFormat = (column: any, summary: any) => {
42
- if (column.subtotalFmt) return getFormatObjectFromString(column.subtotalFmt)
43
- if (column.totalFmt) return getFormatObjectFromString(column.totalFmt)
44
- if (column.fmt) return getFormatObjectFromString(column.fmt, summary.format?.valueType)
45
- return summary.format
46
- }
47
50
  </script>
48
51
 
49
52
  <tr
@@ -63,8 +66,6 @@
63
66
  {/if}
64
67
 
65
68
  {#each orderedColumns as column, index (index)}
66
- {@const summary = safeExtractColumn(column, columnSummary)}
67
- {@const format = resolveFormat(column, summary)}
68
69
  {#if index === 0}
69
70
  {#if rowNumbers}
70
71
  <!-- Covered by the row-number label cell -->
@@ -77,30 +78,21 @@
77
78
  </TableCell>
78
79
  {/if}
79
80
  {:else if subtotals}
80
- <TableCell class={summary.type} {compact} align={column.align}>
81
- {#if ['sum', 'mean', 'median', 'min', 'max', 'weightedMean', 'count', 'countDistinct', undefined].includes(column.totalAgg) || column.subtotalFmt}
82
- {#if column.contentType === 'delta'}
83
- <InlineDelta
84
- value={aggregateColumn(currentGroupData, column.id, column.totalAgg, summary.type, column.weightCol)}
85
- downIsGood={column.downIsGood}
86
- formatObject={format}
87
- columnUnitSummary={summary.columnUnitSummary}
88
- showValue={column.showValue}
89
- showSymbol={column.deltaSymbol}
90
- align={column.align}
91
- neutralMin={column.neutralMin ?? 0}
92
- neutralMax={column.neutralMax ?? 0}
93
- chip={column.chip}
94
- />
95
- {:else}
96
- {formatValue(
97
- aggregateColumn(currentGroupData, column.id, column.totalAgg, summary.type, column.weightCol),
98
- format,
99
- summary.columnUnitSummary,
100
- )}
101
- {/if}
81
+ <TableCell class={column.type} {compact} align={column.align}>
82
+ {#if column.contentType === 'delta'}
83
+ <InlineDelta
84
+ value={getAggregateValue(currentGroupData, column)}
85
+ downIsGood={column.downIsGood}
86
+ field={column.field}
87
+ showValue={column.showValue}
88
+ showSymbol={column.deltaSymbol}
89
+ align={column.align}
90
+ neutralMin={column.neutralMin ?? 0}
91
+ neutralMax={column.neutralMax ?? 0}
92
+ chip={column.chip}
93
+ />
102
94
  {:else}
103
- {column.totalAgg}
95
+ {formatFromField(column.field, getAggregateValue(currentGroupData, column))}
104
96
  {/if}
105
97
  </TableCell>
106
98
  {:else}
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import Column from './Column.svelte'
3
+ import Table from './Table.svelte'
4
+
5
+ interface Props {
6
+ data: any
7
+ tableProps?: Record<string, unknown>
8
+ columns?: Record<string, unknown>[]
9
+ width?: number | string
10
+ }
11
+
12
+ let {data, tableProps = {}, columns = [], width = 880}: Props = $props()
13
+ let wrapperWidth = $derived(typeof width === 'number' ? `${width}px` : width)
14
+ </script>
15
+
16
+ <div class="table-harness" style:width={wrapperWidth}>
17
+ {#if columns.length > 0}
18
+ <Table data={data} {...tableProps}>
19
+ {#each columns as column, index (index)}
20
+ <Column {...column} />
21
+ {/each}
22
+ </Table>
23
+ {:else}
24
+ <Table data={data} {...tableProps} />
25
+ {/if}
26
+ </div>
27
+
28
+ <style>
29
+ .table-harness {
30
+ max-width: 100%;
31
+ }
32
+ </style>
@@ -1,14 +1,12 @@
1
1
  <script lang="ts">
2
2
  import SortIcon from './SortIcon.svelte'
3
- import {safeExtractColumn} from '../component-utilities/tableUtils'
4
- import {toBoolean} from '../component-utilities/convert'
3
+ import {toBoolean} from '../component-utilities/inputUtils'
5
4
 
6
5
  interface Props {
7
6
  rowNumbers?: boolean | string
8
7
  headerColor?: string
9
8
  headerFontColor?: string
10
9
  orderedColumns?: any[]
11
- columnSummary?: any[]
12
10
  sortable?: boolean | string
13
11
  sortClick?: (columnId: string) => () => void
14
12
  formatColumnTitles?: boolean | string
@@ -20,7 +18,7 @@
20
18
 
21
19
  let {
22
20
  rowNumbers: rowNumbersProp = false, headerColor = undefined, headerFontColor = undefined,
23
- orderedColumns = [], columnSummary = [], sortable: sortableProp = true, sortClick = () => () => {},
21
+ orderedColumns = [], sortable: sortableProp = true, sortClick = () => () => {},
24
22
  formatColumnTitles: formatColumnTitlesProp = true, sortObj = {col: null, ascending: null},
25
23
  wrapTitles: wrapTitlesProp = false, compact: compactProp = false, link = undefined,
26
24
  }: Props = $props()
@@ -31,12 +29,11 @@
31
29
  let wrapTitles = $derived(toBoolean(wrapTitlesProp) ?? false)
32
30
  let compact = $derived(toBoolean(compactProp) ?? false)
33
31
 
34
- const getWrapTitleAlignment = (column: any) => {
35
- if (column.align === 'right') return 'header-title--align-end'
36
- if (column.align === 'center') return 'header-title--align-center'
37
- let extracted = safeExtractColumn(column, columnSummary)
38
- if (extracted.type === 'number') return 'header-title--align-end'
39
- return 'header-title--align-start'
32
+ const getHeaderAlignment = (column: any) => {
33
+ if (column.align) return column.align
34
+ if (['sparkline', 'sparkbar', 'sparkarea', 'bar'].includes(column.contentType)) return 'center'
35
+ if (column.type === 'number') return 'right'
36
+ return 'left'
40
37
  }
41
38
 
42
39
  const computeGroupSpans = (columns: any[]) => {
@@ -55,10 +52,10 @@
55
52
  return sortObj.ascending ? 'ascending' : 'descending'
56
53
  }
57
54
 
58
- const resolveHeaderTitle = (column: any, summary: any) => {
55
+ const resolveHeaderTitle = (column: any) => {
59
56
  if (column.title) return column.title
60
- if (formatColumnTitles) return summary.title
61
- return summary.id
57
+ if (formatColumnTitles) return column.defaultTitle ?? column.id
58
+ return column.id
62
59
  }
63
60
 
64
61
  let columnsWithGroupSpan = $derived(computeGroupSpans(orderedColumns))
@@ -71,7 +68,7 @@
71
68
  {#if rowNumbers}
72
69
  <th class={`header-index ${compact ? 'header-index--compact' : ''}`} style:background-color={headerColor}></th>
73
70
  {/if}
74
- {#each columnsWithGroupSpan as column (column.id)}
71
+ {#each columnsWithGroupSpan as column (column.identifier ?? column.id)}
75
72
  {#if column.colGroup && column.isNewGroup}
76
73
  <th class="header-group" colspan={column.span}>
77
74
  <div class="header-group__label">{column.colGroup}</div>
@@ -95,33 +92,28 @@
95
92
  style:color={headerFontColor}
96
93
  ></th>
97
94
  {/if}
98
- {#each orderedColumns as column (column.id)}
99
- {@const summary = safeExtractColumn(column, columnSummary)}
95
+ {#each orderedColumns as column (column.identifier ?? column.id)}
100
96
  <th
101
97
  role="columnheader"
102
- class={`header-cell ${summary.type ?? ''} ${compact ? 'header-cell--compact' : ''}`}
98
+ class={`header-cell ${column.type ?? ''} ${compact ? 'header-cell--compact' : ''}`}
103
99
  style:color={headerFontColor}
104
100
  style:background={headerColor}
105
- style:text-align={column.align ?? (['sparkline', 'sparkbar', 'sparkarea', 'bar'].includes(column.contentType) ? 'center' : undefined)}
101
+ style:text-align={getHeaderAlignment(column)}
106
102
  style:cursor={sortable ? 'pointer' : 'auto'}
107
103
  onclick={sortable ? sortClick(column.id) : undefined}
108
104
  aria-sort={getAriaSortValue(column.id)}
109
105
  >
110
- <div class={`header-title ${wrapTitles || column.wrapTitle ? 'header-title--wrap' : ''} ${wrapTitles || column.wrapTitle ? getWrapTitleAlignment(column) : ''}`.trim()}>
111
- <span class={`header-title__text ${wrapTitles || column.wrapTitle ? 'header-title__text--wrap' : ''}`}>
112
- {resolveHeaderTitle(column, summary)}
113
- {#if column.description}
114
- <span class="header-title__info" title={column.description}>ⓘ</span>
115
- {/if}
116
- </span>
106
+ <span class={`header-title__text ${wrapTitles || column.wrapTitle ? 'header-title__text--wrap' : ''}`}>
107
+ {resolveHeaderTitle(column)}
108
+ {#if column.description}
109
+ <span class="header-title__info" title={column.description}>ⓘ</span>
110
+ {/if}
111
+ </span>
112
+ {#if sortObj.col === column.id}
117
113
  <span class="header-sort-indicator">
118
- {#if sortObj.col === column.id}
119
- <SortIcon ascending={sortObj.ascending ?? undefined} />
120
- {:else}
121
- <span class="header-sort-placeholder"><SortIcon ascending /></span>
122
- {/if}
114
+ <SortIcon ascending={sortObj.ascending ?? undefined} />
123
115
  </span>
124
- </div>
116
+ {/if}
125
117
  </th>
126
118
  {/each}
127
119
  {#if link}
@@ -178,40 +170,24 @@
178
170
  }
179
171
 
180
172
  .header-cell {
173
+ position: relative;
181
174
  padding: 2px 13px 2px 6px;
182
175
  vertical-align: bottom;
183
176
  }
184
177
 
185
- .header-cell--compact {
186
- padding: 1px 6px 1px 1px;
187
- font-size: 12px;
178
+ .header-cell:first-child {
179
+ padding-left: 3px;
188
180
  }
189
181
 
190
- .header-title {
191
- display: flex;
192
- align-items: flex-end;
193
- justify-content: space-between;
194
- gap: 4px;
195
- }
196
-
197
- .header-title--wrap {
198
- align-items: stretch;
199
- }
200
-
201
- .header-title--align-end {
202
- justify-content: flex-end;
203
- }
204
-
205
- .header-title--align-center {
206
- justify-content: center;
207
- }
208
-
209
- .header-title--align-start {
210
- justify-content: flex-start;
182
+ .header-cell--compact {
183
+ padding: 1px 16.5px 1px 1px;
184
+ font-size: 12px;
211
185
  }
212
186
 
213
187
  .header-title__text {
214
188
  display: inline-block;
189
+ max-width: 100%;
190
+ text-align: inherit;
215
191
  letter-spacing: -0.015em;
216
192
  }
217
193
 
@@ -227,14 +203,13 @@
227
203
  }
228
204
 
229
205
  .header-sort-indicator {
206
+ position: absolute;
207
+ right: 1px;
208
+ bottom: 4px;
230
209
  display: inline-flex;
231
210
  align-items: center;
232
211
  }
233
212
 
234
- .header-sort-placeholder {
235
- visibility: hidden;
236
- }
237
-
238
213
  .header-link-cell {
239
214
  width: 24px;
240
215
  }