@graphenedata/cli 0.0.1

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 (123) hide show
  1. package/LICENSE.md +100 -0
  2. package/THIRD_PARTY_NOTICES.md +12 -0
  3. package/cli.ts +157 -0
  4. package/dist/cli/cli.js +43 -0
  5. package/dist/docs/data_apps/components/charts/annotations.md +673 -0
  6. package/dist/docs/data_apps/components/charts/area-chart.md +202 -0
  7. package/dist/docs/data_apps/components/charts/bar-chart.md +317 -0
  8. package/dist/docs/data_apps/components/charts/box-plot.md +190 -0
  9. package/dist/docs/data_apps/components/charts/bubble-chart.md +151 -0
  10. package/dist/docs/data_apps/components/charts/calendar-heatmap.md +112 -0
  11. package/dist/docs/data_apps/components/charts/custom-echarts.md +308 -0
  12. package/dist/docs/data_apps/components/charts/echarts-options.md +217 -0
  13. package/dist/docs/data_apps/components/charts/funnel-chart.md +106 -0
  14. package/dist/docs/data_apps/components/charts/heatmap.md +180 -0
  15. package/dist/docs/data_apps/components/charts/histogram.md +107 -0
  16. package/dist/docs/data_apps/components/charts/line-chart.md +265 -0
  17. package/dist/docs/data_apps/components/charts/mixed-type-charts.md +240 -0
  18. package/dist/docs/data_apps/components/charts/sankey-diagram.md +301 -0
  19. package/dist/docs/data_apps/components/charts/scatter-plot.md +134 -0
  20. package/dist/docs/data_apps/components/charts/sparkline.md +68 -0
  21. package/dist/docs/data_apps/components/data/big-value.md +153 -0
  22. package/dist/docs/data_apps/components/data/delta.md +89 -0
  23. package/dist/docs/data_apps/components/data/table.md +470 -0
  24. package/dist/docs/data_apps/components/data/value.md +97 -0
  25. package/dist/docs/data_apps/components/inputs/button-group.md +154 -0
  26. package/dist/docs/data_apps/components/inputs/checkbox.md +52 -0
  27. package/dist/docs/data_apps/components/inputs/date-input.md +131 -0
  28. package/dist/docs/data_apps/components/inputs/date-range.md +124 -0
  29. package/dist/docs/data_apps/components/inputs/dimension-grid.md +67 -0
  30. package/dist/docs/data_apps/components/inputs/dropdown.md +199 -0
  31. package/dist/docs/data_apps/components/inputs/index.md +3 -0
  32. package/dist/docs/data_apps/components/inputs/slider.md +126 -0
  33. package/dist/docs/data_apps/components/inputs/text-input.md +86 -0
  34. package/dist/docs/data_apps/components/maps/area-map.md +397 -0
  35. package/dist/docs/data_apps/components/maps/base-map.md +269 -0
  36. package/dist/docs/data_apps/components/maps/bubble-map.md +361 -0
  37. package/dist/docs/data_apps/components/maps/point-map.md +326 -0
  38. package/dist/docs/data_apps/components/maps/us-map.md +167 -0
  39. package/dist/docs/data_apps/components/ui/accordion.md +116 -0
  40. package/dist/docs/data_apps/components/ui/alert.md +37 -0
  41. package/dist/docs/data_apps/components/ui/big-link.md +19 -0
  42. package/dist/docs/data_apps/components/ui/details.md +58 -0
  43. package/dist/docs/data_apps/components/ui/download-data.md +41 -0
  44. package/dist/docs/data_apps/components/ui/embed.md +47 -0
  45. package/dist/docs/data_apps/components/ui/grid.md +45 -0
  46. package/dist/docs/data_apps/components/ui/image.md +61 -0
  47. package/dist/docs/data_apps/components/ui/info.md +47 -0
  48. package/dist/docs/data_apps/components/ui/last-refreshed.md +28 -0
  49. package/dist/docs/data_apps/components/ui/link-button.md +20 -0
  50. package/dist/docs/data_apps/components/ui/link.md +40 -0
  51. package/dist/docs/data_apps/components/ui/modal.md +57 -0
  52. package/dist/docs/data_apps/components/ui/note.md +32 -0
  53. package/dist/docs/data_apps/components/ui/print-format-components.md +85 -0
  54. package/dist/docs/data_apps/components/ui/tabs.md +122 -0
  55. package/dist/docs/graphene.md +129 -0
  56. package/dist/ui/app.css +332 -0
  57. package/dist/ui/assets/favicon.ico +0 -0
  58. package/dist/ui/component-utilities/autoFormatting.js +301 -0
  59. package/dist/ui/component-utilities/builtInFormats.js +482 -0
  60. package/dist/ui/component-utilities/chartContext.js +12 -0
  61. package/dist/ui/component-utilities/chartWindowDebug.js +21 -0
  62. package/dist/ui/component-utilities/checkInputs.js +95 -0
  63. package/dist/ui/component-utilities/convert.js +15 -0
  64. package/dist/ui/component-utilities/dateParsing.js +57 -0
  65. package/dist/ui/component-utilities/dropdownContext.ts +1 -0
  66. package/dist/ui/component-utilities/echarts.js +262 -0
  67. package/dist/ui/component-utilities/echartsThemes.js +453 -0
  68. package/dist/ui/component-utilities/formatTitle.js +24 -0
  69. package/dist/ui/component-utilities/formatting.js +258 -0
  70. package/dist/ui/component-utilities/getColumnExtents.js +79 -0
  71. package/dist/ui/component-utilities/getColumnSummary.js +67 -0
  72. package/dist/ui/component-utilities/getCompletedData.js +114 -0
  73. package/dist/ui/component-utilities/getDistinctCount.js +7 -0
  74. package/dist/ui/component-utilities/getDistinctValues.js +15 -0
  75. package/dist/ui/component-utilities/getSeriesConfig.js +236 -0
  76. package/dist/ui/component-utilities/getSortedData.js +7 -0
  77. package/dist/ui/component-utilities/getStackPercentages.js +43 -0
  78. package/dist/ui/component-utilities/getStackedData.js +17 -0
  79. package/dist/ui/component-utilities/getYAxisIndex.js +15 -0
  80. package/dist/ui/component-utilities/globalContexts.js +1 -0
  81. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +119 -0
  82. package/dist/ui/component-utilities/inputUtils.ts +25 -0
  83. package/dist/ui/component-utilities/replaceNulls.js +14 -0
  84. package/dist/ui/component-utilities/tableUtils.ts +120 -0
  85. package/dist/ui/component-utilities/themeStores.ts +116 -0
  86. package/dist/ui/components/Area.svelte +174 -0
  87. package/dist/ui/components/AreaChart.svelte +150 -0
  88. package/dist/ui/components/Bar.svelte +326 -0
  89. package/dist/ui/components/BarChart.svelte +194 -0
  90. package/dist/ui/components/BigValue.svelte +69 -0
  91. package/dist/ui/components/Chart.svelte +1070 -0
  92. package/dist/ui/components/Column.svelte +172 -0
  93. package/dist/ui/components/DateRange.svelte +324 -0
  94. package/dist/ui/components/Dropdown.svelte +738 -0
  95. package/dist/ui/components/DropdownOption.svelte +21 -0
  96. package/dist/ui/components/ECharts.svelte +77 -0
  97. package/dist/ui/components/ErrorChart.svelte +54 -0
  98. package/dist/ui/components/GrapheneQuery.svelte +12 -0
  99. package/dist/ui/components/InlineDelta.svelte +150 -0
  100. package/dist/ui/components/Line.svelte +210 -0
  101. package/dist/ui/components/LineChart.svelte +178 -0
  102. package/dist/ui/components/PieChart.svelte +36 -0
  103. package/dist/ui/components/QueryLoad.svelte +82 -0
  104. package/dist/ui/components/Row.svelte +14 -0
  105. package/dist/ui/components/SortIcon.svelte +32 -0
  106. package/dist/ui/components/Table.svelte +19 -0
  107. package/dist/ui/components/TableCell.svelte +75 -0
  108. package/dist/ui/components/TableGroupRow.svelte +136 -0
  109. package/dist/ui/components/TableGroupToggle.svelte +42 -0
  110. package/dist/ui/components/TableHeader.svelte +242 -0
  111. package/dist/ui/components/TableRow.svelte +283 -0
  112. package/dist/ui/components/TableSubtotalRow.svelte +62 -0
  113. package/dist/ui/components/TableTotalRow.svelte +88 -0
  114. package/dist/ui/components/TextInput.svelte +92 -0
  115. package/dist/ui/components/_Table.svelte +516 -0
  116. package/dist/ui/internal/clientCache.ts +43 -0
  117. package/dist/ui/internal/queryEngine.ts +169 -0
  118. package/dist/ui/internal/telemetry.ts +28 -0
  119. package/dist/ui/internal/theme.ts +88 -0
  120. package/dist/ui/layout.svelte +3 -0
  121. package/dist/ui/playwright.config.ts +30 -0
  122. package/dist/ui/web.js +106 -0
  123. package/package.json +71 -0
@@ -0,0 +1,36 @@
1
+ <script>
2
+ import ECharts from './ECharts.svelte'
3
+ import QueryLoad from './QueryLoad.svelte'
4
+
5
+ export let data
6
+ export let category
7
+ export let value
8
+ export let title = undefined
9
+ export let subtitle = undefined
10
+ export let printEchartsConfig = undefined
11
+ export let echartsOptions = undefined
12
+ export let seriesOptions = undefined
13
+ export let seriesColors = undefined
14
+ $: void printEchartsConfig
15
+ </script>
16
+
17
+ <style></style>
18
+
19
+ <QueryLoad data={data} fields={[category, value]} let:loaded>
20
+ <ECharts data={loaded} {echartsOptions} {seriesOptions} {seriesColors} config={{
21
+ title: {
22
+ text: title,
23
+ subtext: subtitle,
24
+ },
25
+ tooltip: {
26
+ formatter: '{b}: {c} ({d}%)',
27
+ },
28
+ series: [
29
+ {
30
+ type: 'pie',
31
+ radius: ['40%', '70%'],
32
+ data: [...loaded.map(r => ({name: r[category], value: r[value]}))],
33
+ },
34
+ ],
35
+ }} />
36
+ </QueryLoad>
@@ -0,0 +1,82 @@
1
+ <script lang="ts">
2
+ import ErrorChart from './ErrorChart.svelte'
3
+ import {onDestroy, onMount} from 'svelte'
4
+
5
+ export let data: string | {rows?: any[]}
6
+ export let height = 200
7
+ export let fields: string[] = []
8
+
9
+ let errors: Error[] | null = null
10
+ let loaded: any[] | null = null
11
+
12
+ let handleResults = (data) => {
13
+ errors = data.errors
14
+ loaded = data.rows
15
+ }
16
+
17
+ onMount(() => {
18
+ if (typeof data !== 'string') {
19
+ loaded = data.rows
20
+ } else {
21
+ let usedFields = fields.filter(f => !!f)
22
+ if (usedFields.length == 0) usedFields = ['*']
23
+ window.$GRAPHENE.query(data, usedFields, handleResults)
24
+ }
25
+ })
26
+
27
+ onDestroy(() => {
28
+ window.$GRAPHENE.unsubscribe(handleResults)
29
+ })
30
+ </script>
31
+
32
+ {#if errors}
33
+ <ErrorChart title="Error" error={errors[0]} />
34
+ {:else if !loaded}
35
+ <div class='ql-skeleton' style={`height:${height}px`} role="status" aria-live="polite">
36
+ <span class="ql-skeleton__pulse" />
37
+ </div>
38
+ {:else if loaded.length == 0}
39
+ <div class="empty-chart" role="note">Dataset is empty - query ran successfully, but no data was returned from the database</div>
40
+ {:else}
41
+ <slot loaded={loaded} />
42
+ {/if}
43
+
44
+ <style>
45
+ .ql-skeleton {
46
+ width: 100%;
47
+ position: relative;
48
+ overflow: hidden;
49
+ background: var(--chart-skeleton-bg, #f3f4f6);
50
+ border-radius: 4px;
51
+ }
52
+
53
+ .ql-skeleton__pulse {
54
+ position: absolute;
55
+ inset: 0;
56
+ transform: translateX(-100%);
57
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.55) 50%, rgba(255, 255, 255, 0) 100%);
58
+ animation: ql-pulse 1.4s ease-in-out infinite;
59
+ content: '';
60
+ }
61
+
62
+ @keyframes ql-pulse {
63
+ 0% {
64
+ transform: translateX(-100%);
65
+ }
66
+ 100% {
67
+ transform: translateX(100%);
68
+ }
69
+ }
70
+
71
+ .empty-chart {
72
+ width: 100%;
73
+ padding: 12px;
74
+ margin: 8px 0;
75
+ border: 1px dashed rgba(107, 114, 128, 0.6);
76
+ border-radius: 4px;
77
+ font-size: 12px;
78
+ color: rgba(75, 85, 99, 0.9);
79
+ text-align: center;
80
+ background: rgba(243, 244, 246, 0.6);
81
+ }
82
+ </style>
@@ -0,0 +1,14 @@
1
+ <div><slot></slot></div>
2
+
3
+ <style>
4
+ div {
5
+ display: flex;
6
+ flex-direction: row;
7
+ gap: 1rem;
8
+ }
9
+
10
+ /* Ensure styles apply to slotted children (not scoped) */
11
+ div > :global(*) {
12
+ flex: 1 1 0;
13
+ }
14
+ </style>
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ export let ascending: boolean | undefined = undefined
3
+ </script>
4
+
5
+ <svg
6
+ class="sort-icon"
7
+ viewBox="0 0 16 16"
8
+ aria-hidden="true"
9
+ >
10
+ {#if ascending}
11
+ <path
12
+ d="M8 5.2 3.2 10a.75.75 0 0 0 1.06 1.06L8 7.32l3.74 3.74a.75.75 0 1 0 1.06-1.06Z"
13
+ fill="currentColor"
14
+ fill-rule="evenodd"
15
+ />
16
+ {:else}
17
+ <path
18
+ d="M8 10.8 12.8 6a.75.75 0 0 0-1.06-1.06L8 8.68 4.26 4.94A.75.75 0 0 0 3.2 6Z"
19
+ fill="currentColor"
20
+ fill-rule="evenodd"
21
+ />
22
+ {/if}
23
+ </svg>
24
+
25
+ <style>
26
+ .sort-icon {
27
+ display: inline-block;
28
+ width: 10px;
29
+ height: 10px;
30
+ margin-bottom: 2px;
31
+ }
32
+ </style>
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import QueryLoad from './QueryLoad.svelte'
3
+ import TableInner from './_Table.svelte'
4
+
5
+ export let data: string
6
+
7
+ const restProps: Record<string, unknown> = $$restProps
8
+ $: spreadProps = Object.fromEntries(Object.entries(restProps).filter(([, value]) => value !== undefined))
9
+ </script>
10
+
11
+ <QueryLoad {data} let:loaded>
12
+ {#if $$slots.default}
13
+ <TableInner {...spreadProps} data={loaded}>
14
+ <slot />
15
+ </TableInner>
16
+ {:else}
17
+ <TableInner {...spreadProps} data={loaded} />
18
+ {/if}
19
+ </QueryLoad>
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ export let dataType: string | undefined = undefined
3
+ export let align: string | undefined = undefined
4
+ export let height: string | number | undefined = undefined
5
+ export let width: string | number | undefined = undefined
6
+ export let wrap: boolean | undefined = undefined
7
+ export let verticalAlign: 'top' | 'middle' | 'bottom' | string = 'middle'
8
+ export let rowSpan = 1
9
+ export let colSpan = 1
10
+ export let show = true
11
+ export let cellColor: string | undefined = undefined
12
+ export let fontColor: string | undefined = undefined
13
+ export let topBorder: string | undefined = undefined
14
+ export let paddingLeft: string | undefined = undefined
15
+ export let borderBottom: string | undefined = undefined
16
+ export let compact = false
17
+ </script>
18
+
19
+ <td
20
+ role="cell"
21
+ rowspan={rowSpan}
22
+ colspan={colSpan}
23
+ class={`table-cell ${dataType ?? ''} ${compact ? 'table-cell--compact' : ''} ${topBorder ?? ''} ${$$restProps.class ?? ''}`.trim()}
24
+ style:text-align={align}
25
+ style:height={height}
26
+ style:width={width}
27
+ style:white-space={wrap ? 'normal' : 'nowrap'}
28
+ style:vertical-align={verticalAlign}
29
+ style:display={show ? undefined : 'none'}
30
+ style:background-color={cellColor}
31
+ style:color={fontColor}
32
+ style:padding-left={paddingLeft}
33
+ style:border-top={topBorder}
34
+ style:border-bottom={borderBottom}
35
+ >
36
+ <slot />
37
+ </td>
38
+
39
+ <style>
40
+ .table-cell {
41
+ padding: 2px 13px 2px 6px;
42
+ font-variant-numeric: tabular-nums;
43
+ overflow: hidden;
44
+ text-overflow: ellipsis;
45
+ }
46
+
47
+ .table-cell:first-child {
48
+ padding-left: 3px;
49
+ }
50
+
51
+ .table-cell--compact {
52
+ padding: 1px 16.5px 1px 1px;
53
+ font-size: 12px;
54
+ }
55
+
56
+ .string,
57
+ .date,
58
+ .boolean {
59
+ text-align: left;
60
+ }
61
+
62
+ .number {
63
+ text-align: right;
64
+ }
65
+
66
+ .index {
67
+ color: var(--color-base-content-muted, #6b7280);
68
+ text-align: left;
69
+ max-width: min-content;
70
+ }
71
+
72
+ td:focus {
73
+ outline: none;
74
+ }
75
+ </style>
@@ -0,0 +1,136 @@
1
+ <script lang="ts">
2
+ import {createEventDispatcher} from 'svelte'
3
+ import TableCell from './TableCell.svelte'
4
+ import TableGroupToggle from './TableGroupToggle.svelte'
5
+ import InlineDelta from './InlineDelta.svelte'
6
+ import {aggregateColumn, safeExtractColumn} from '../component-utilities/tableUtils'
7
+ import {formatValue, getFormatObjectFromString} from '../component-utilities/formatting.js'
8
+ import {toBoolean} from '../component-utilities/convert'
9
+
10
+ export let groupName: string
11
+ export let currentGroupData: any[] = []
12
+ export let toggled = true
13
+ export let columnSummary: any[] = []
14
+ export let rowNumbers: boolean | string | undefined = undefined
15
+ export let rowColor: string | undefined = undefined
16
+ export let subtotals: boolean | string | undefined = true
17
+ export let orderedColumns: any[] = []
18
+ export let compact: boolean | string | undefined = undefined
19
+
20
+ rowNumbers = toBoolean(rowNumbers) ?? false
21
+ subtotals = toBoolean(subtotals) ?? true
22
+ compact = toBoolean(compact)
23
+
24
+ const dispatch = createEventDispatcher<{toggle: {groupName: string}}>()
25
+ const toggleGroup = () => dispatch('toggle', {groupName})
26
+
27
+ const handleKeydown = (event: KeyboardEvent) => {
28
+ if (event.key === 'Enter' || event.key === ' ') {
29
+ event.preventDefault()
30
+ toggleGroup()
31
+ }
32
+ }
33
+
34
+ const resolveFormat = (column, summary) => {
35
+ if (column.subtotalFmt) return getFormatObjectFromString(column.subtotalFmt)
36
+ if (column.totalFmt) return getFormatObjectFromString(column.totalFmt)
37
+ if (column.fmt) return getFormatObjectFromString(column.fmt, summary.format?.valueType)
38
+ return summary.format
39
+ }
40
+ </script>
41
+
42
+ <tr
43
+ class="group-row"
44
+ tabindex="0"
45
+ on:click={toggleGroup}
46
+ on:keydown={handleKeydown}
47
+ style:background-color={rowColor}
48
+ >
49
+ {#if rowNumbers}
50
+ <TableCell class="group-row__label" {compact} colSpan={2}>
51
+ <div class="group-row__title">
52
+ <span class="group-row__icon"><TableGroupToggle {toggled} /></span>
53
+ {groupName}
54
+ </div>
55
+ </TableCell>
56
+ {/if}
57
+
58
+ {#each orderedColumns as column, index (index)}
59
+ {@const summary = safeExtractColumn(column, columnSummary)}
60
+ {@const format = resolveFormat(column, summary)}
61
+ {#if index === 0}
62
+ {#if rowNumbers}
63
+ <!-- Covered by the row-number label cell -->
64
+ {:else}
65
+ <TableCell class="group-row__label" {compact} paddingLeft="1px">
66
+ <div class="group-row__title">
67
+ <span class="group-row__icon"><TableGroupToggle {toggled} /></span>
68
+ {groupName}
69
+ </div>
70
+ </TableCell>
71
+ {/if}
72
+ {:else if subtotals}
73
+ <TableCell class={summary.type} {compact} align={column.align}>
74
+ {#if ['sum', 'mean', 'median', 'min', 'max', 'weightedMean', 'count', 'countDistinct', undefined].includes(column.totalAgg) || column.subtotalFmt}
75
+ {#if column.contentType === 'delta'}
76
+ <InlineDelta
77
+ value={aggregateColumn(currentGroupData, column.id, column.totalAgg, summary.type, column.weightCol)}
78
+ downIsGood={column.downIsGood}
79
+ formatObject={format}
80
+ columnUnitSummary={summary.columnUnitSummary}
81
+ showValue={column.showValue}
82
+ showSymbol={column.deltaSymbol}
83
+ align={column.align}
84
+ neutralMin={column.neutralMin ?? 0}
85
+ neutralMax={column.neutralMax ?? 0}
86
+ chip={column.chip}
87
+ />
88
+ {:else}
89
+ {formatValue(
90
+ aggregateColumn(currentGroupData, column.id, column.totalAgg, summary.type, column.weightCol),
91
+ format,
92
+ summary.columnUnitSummary,
93
+ )}
94
+ {/if}
95
+ {:else}
96
+ {column.totalAgg}
97
+ {/if}
98
+ </TableCell>
99
+ {:else}
100
+ <TableCell />
101
+ {/if}
102
+ {/each}
103
+ </tr>
104
+
105
+ <style>
106
+ .group-row {
107
+ font-weight: 600;
108
+ border-top: 1px solid rgba(229, 231, 235, 1);
109
+ cursor: pointer;
110
+ }
111
+
112
+ .group-row:focus {
113
+ outline: 2px solid var(--color-primary, #2563eb);
114
+ outline-offset: 1px;
115
+ }
116
+
117
+ :global(.group-row__label) {
118
+ padding: 3px 6px;
119
+ }
120
+
121
+ .group-row__title {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ gap: 8px;
125
+ }
126
+
127
+ .group-row__icon {
128
+ display: inline-flex;
129
+ }
130
+
131
+ @media print {
132
+ .group-row__icon {
133
+ display: none;
134
+ }
135
+ }
136
+ </style>
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import {getThemeStores} from '../component-utilities/themeStores'
3
+
4
+ export let toggled = false
5
+ export let color: string | undefined = undefined
6
+ export let size = 10
7
+
8
+ const {theme, resolveColor} = getThemeStores()
9
+ let colorStore = resolveColor(color)
10
+
11
+ $: colorStore = resolveColor(color)
12
+ $: fill = $colorStore ?? $theme.colors['base-content']
13
+ </script>
14
+
15
+ <span aria-expanded={toggled} class="group-toggle">
16
+ <svg viewBox="0 0 16 16" width={size} height={size}>
17
+ <path
18
+ fill={fill}
19
+ fill-rule="evenodd"
20
+ d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06z"
21
+ />
22
+ </svg>
23
+ </span>
24
+
25
+ <style>
26
+ .group-toggle {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ margin: auto 0 auto 0;
31
+ }
32
+
33
+ svg {
34
+ display: inline-block;
35
+ vertical-align: middle;
36
+ transition: transform 0.15s ease-in;
37
+ }
38
+
39
+ [aria-expanded='true'] svg {
40
+ transform: rotate(90deg);
41
+ }
42
+ </style>
@@ -0,0 +1,242 @@
1
+ <script lang="ts">
2
+ import SortIcon from './SortIcon.svelte'
3
+ import {safeExtractColumn} from '../component-utilities/tableUtils'
4
+ import {toBoolean} from '../component-utilities/convert'
5
+
6
+ export let rowNumbers: boolean | string | undefined = false
7
+ export let headerColor: string | undefined = undefined
8
+ export let headerFontColor: string | undefined = undefined
9
+ export let orderedColumns: any[] = []
10
+ export let columnSummary: any[] = []
11
+ export let sortable: boolean | string | undefined = true
12
+ export let sortClick: (columnId: string) => () => void = () => () => {}
13
+ export let formatColumnTitles: boolean | string | undefined = true
14
+ export let sortObj: {col: string | null; ascending: boolean | null} = {col: null, ascending: null}
15
+ export let wrapTitles: boolean | string | undefined = false
16
+ export let compact: boolean | string | undefined = false
17
+ export let link: string | undefined = undefined
18
+
19
+ rowNumbers = toBoolean(rowNumbers) ?? false
20
+ sortable = toBoolean(sortable) ?? true
21
+ formatColumnTitles = toBoolean(formatColumnTitles) ?? true
22
+ wrapTitles = toBoolean(wrapTitles) ?? false
23
+ compact = toBoolean(compact) ?? false
24
+
25
+ const getWrapTitleAlignment = (column: any) => {
26
+ if (column.align === 'right') return 'header-title--align-end'
27
+ if (column.align === 'center') return 'header-title--align-center'
28
+ let extracted = safeExtractColumn(column, columnSummary)
29
+ if (extracted.type === 'number') return 'header-title--align-end'
30
+ return 'header-title--align-start'
31
+ }
32
+
33
+ const computeGroupSpans = () => {
34
+ return orderedColumns.map((column, index, array) => {
35
+ let isNewGroup = index === 0 || column.colGroup !== array[index - 1].colGroup
36
+ let span = 1
37
+ if (column.colGroup) {
38
+ for (let i = index + 1; i < array.length && array[i].colGroup === column.colGroup; i++) span += 1
39
+ }
40
+ return {...column, isNewGroup, span: isNewGroup ? span : 0}
41
+ })
42
+ }
43
+
44
+ const getAriaSortValue = (columnId: string) => {
45
+ if (sortObj.col !== columnId) return 'none'
46
+ return sortObj.ascending ? 'ascending' : 'descending'
47
+ }
48
+
49
+ const resolveHeaderTitle = (column: any, summary: any) => {
50
+ if (column.title) return column.title
51
+ if (formatColumnTitles) return summary.title
52
+ return summary.id
53
+ }
54
+
55
+ $: columnsWithGroupSpan = computeGroupSpans()
56
+ </script>
57
+
58
+ <thead>
59
+ {#if columnsWithGroupSpan.length}
60
+ <tr class="header-group-row" style:background-color={headerColor}>
61
+ {#if rowNumbers}
62
+ <th class={`header-index ${compact ? 'header-index--compact' : ''}`} style:background-color={headerColor} />
63
+ {/if}
64
+ {#each columnsWithGroupSpan as column (column.id)}
65
+ {#if column.colGroup && column.isNewGroup}
66
+ <th class="header-group" colspan={column.span}>
67
+ <div class="header-group__label">{column.colGroup}</div>
68
+ </th>
69
+ {:else}
70
+ <th class="header-group--spacer" />
71
+ {/if}
72
+ {/each}
73
+ {#if link}
74
+ <th class="header-group--spacer" />
75
+ {/if}
76
+ </tr>
77
+ {/if}
78
+
79
+ <tr class="header-row">
80
+ {#if rowNumbers}
81
+ <th
82
+ role="columnheader"
83
+ class={`header-index ${compact ? 'header-index--compact' : ''}`}
84
+ style:background-color={headerColor}
85
+ style:color={headerFontColor}
86
+ />
87
+ {/if}
88
+ {#each orderedColumns as column (column.id)}
89
+ {@const summary = safeExtractColumn(column, columnSummary)}
90
+ <th
91
+ role="columnheader"
92
+ class={`header-cell ${summary.type ?? ''} ${compact ? 'header-cell--compact' : ''}`}
93
+ style:color={headerFontColor}
94
+ style:background={headerColor}
95
+ style:text-align={column.align ?? (['sparkline', 'sparkbar', 'sparkarea', 'bar'].includes(column.contentType) ? 'center' : undefined)}
96
+ style:cursor={sortable ? 'pointer' : 'auto'}
97
+ on:click={sortable ? sortClick(column.id) : undefined}
98
+ aria-sort={getAriaSortValue(column.id)}
99
+ >
100
+ <div class={`header-title ${wrapTitles || column.wrapTitle ? 'header-title--wrap' : ''} ${wrapTitles || column.wrapTitle ? getWrapTitleAlignment(column) : ''}`.trim()}>
101
+ <span class={`header-title__text ${wrapTitles || column.wrapTitle ? 'header-title__text--wrap' : ''}`}>
102
+ {resolveHeaderTitle(column, summary)}
103
+ {#if column.description}
104
+ <span class="header-title__info" title={column.description}>ⓘ</span>
105
+ {/if}
106
+ </span>
107
+ <span class="header-sort-indicator">
108
+ {#if sortObj.col === column.id}
109
+ <SortIcon ascending={sortObj.ascending ?? undefined} />
110
+ {:else}
111
+ <span class="header-sort-placeholder"><SortIcon ascending /></span>
112
+ {/if}
113
+ </span>
114
+ </div>
115
+ </th>
116
+ {/each}
117
+ {#if link}
118
+ <th class="header-link-cell"><span class="sr-only">Links</span></th>
119
+ {/if}
120
+ </tr>
121
+ </thead>
122
+
123
+ <style>
124
+ thead {
125
+ background: var(--table-header-background, inherit);
126
+ }
127
+
128
+ .header-group-row {
129
+ height: 26px;
130
+ }
131
+
132
+ .header-group {
133
+ padding: 0;
134
+ }
135
+
136
+ .header-group__label {
137
+ padding: 4px 6px 2px;
138
+ border-bottom: 1px solid rgba(107, 114, 128, 0.4);
139
+ font-weight: 500;
140
+ white-space: nowrap;
141
+ }
142
+
143
+ .header-group--spacer {
144
+ padding: 0;
145
+ }
146
+
147
+ .header-row {
148
+ border-bottom: 1px solid rgba(107, 114, 128, 0.6);
149
+ }
150
+
151
+ th {
152
+ white-space: nowrap;
153
+ overflow: hidden;
154
+ text-overflow: ellipsis;
155
+ font-weight: 500;
156
+ }
157
+
158
+ .header-index {
159
+ color: var(--color-base-content-muted, #6b7280);
160
+ text-align: left;
161
+ padding: 2px 8px 2px 3px;
162
+ max-width: min-content;
163
+ }
164
+
165
+ .header-index--compact {
166
+ padding: 1px 4px;
167
+ font-size: 12px;
168
+ }
169
+
170
+ .header-cell {
171
+ padding: 2px 13px 2px 6px;
172
+ vertical-align: bottom;
173
+ }
174
+
175
+ .header-cell--compact {
176
+ padding: 1px 6px 1px 1px;
177
+ font-size: 12px;
178
+ }
179
+
180
+ .header-title {
181
+ display: flex;
182
+ align-items: flex-end;
183
+ justify-content: space-between;
184
+ gap: 4px;
185
+ }
186
+
187
+ .header-title--wrap {
188
+ align-items: stretch;
189
+ }
190
+
191
+ .header-title--align-end {
192
+ justify-content: flex-end;
193
+ }
194
+
195
+ .header-title--align-center {
196
+ justify-content: center;
197
+ }
198
+
199
+ .header-title--align-start {
200
+ justify-content: flex-start;
201
+ }
202
+
203
+ .header-title__text {
204
+ display: inline-block;
205
+ letter-spacing: -0.015em;
206
+ }
207
+
208
+ .header-title__text--wrap {
209
+ white-space: normal;
210
+ }
211
+
212
+ .header-title__info {
213
+ margin-left: 4px;
214
+ cursor: help;
215
+ font-size: 0.75em;
216
+ color: var(--color-base-content-muted, #6b7280);
217
+ }
218
+
219
+ .header-sort-indicator {
220
+ display: inline-flex;
221
+ align-items: center;
222
+ }
223
+
224
+ .header-sort-placeholder {
225
+ visibility: hidden;
226
+ }
227
+
228
+ .header-link-cell {
229
+ width: 24px;
230
+ }
231
+
232
+ .sr-only {
233
+ position: absolute;
234
+ width: 1px;
235
+ height: 1px;
236
+ padding: 0;
237
+ margin: -1px;
238
+ overflow: hidden;
239
+ clip: rect(0, 0, 0, 0);
240
+ border: 0;
241
+ }
242
+ </style>