@graphenedata/cli 0.0.12 → 0.0.14

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 (46) hide show
  1. package/dist/cli/cli.js +8591 -1214
  2. package/dist/docs/base.md +98 -0
  3. package/dist/docs/cli.md +22 -0
  4. package/dist/docs/graphene.md +10 -10
  5. package/dist/ui/component-utilities/echarts.js +2 -3
  6. package/dist/ui/component-utilities/formatting.js +3 -11
  7. package/dist/ui/component-utilities/getSeriesConfig.js +2 -1
  8. package/dist/ui/components/Area.svelte +188 -151
  9. package/dist/ui/components/AreaChart.svelte +43 -79
  10. package/dist/ui/components/Bar.svelte +273 -255
  11. package/dist/ui/components/BarChart.svelte +58 -112
  12. package/dist/ui/components/BigValue.svelte +13 -7
  13. package/dist/ui/components/Chart.svelte +280 -317
  14. package/dist/ui/components/Column.svelte +102 -113
  15. package/dist/ui/components/DateRange.svelte +37 -27
  16. package/dist/ui/components/Dropdown.svelte +77 -57
  17. package/dist/ui/components/DropdownOption.svelte +10 -7
  18. package/dist/ui/components/ECharts.svelte +23 -16
  19. package/dist/ui/components/ErrorChart.svelte +85 -21
  20. package/dist/ui/components/GrapheneQuery.svelte +7 -3
  21. package/dist/ui/components/InlineDelta.svelte +53 -34
  22. package/dist/ui/components/Line.svelte +192 -178
  23. package/dist/ui/components/LineChart.svelte +53 -96
  24. package/dist/ui/components/PieChart.svelte +26 -15
  25. package/dist/ui/components/QueryLoad.svelte +15 -10
  26. package/dist/ui/components/SortIcon.svelte +5 -1
  27. package/dist/ui/components/Table.svelte +15 -9
  28. package/dist/ui/components/TableCell.svelte +30 -17
  29. package/dist/ui/components/TableGroupRow.svelte +26 -19
  30. package/dist/ui/components/TableGroupToggle.svelte +9 -6
  31. package/dist/ui/components/TableHeader.svelte +37 -27
  32. package/dist/ui/components/TableRow.svelte +30 -20
  33. package/dist/ui/components/TableSubtotalRow.svelte +16 -9
  34. package/dist/ui/components/TableTotalRow.svelte +18 -11
  35. package/dist/ui/components/TextInput.svelte +23 -20
  36. package/dist/ui/components/_Table.svelte +303 -260
  37. package/dist/ui/internal/LocalApp.svelte +40 -0
  38. package/dist/ui/internal/NavSidebar.svelte +27 -30
  39. package/dist/ui/internal/PageError.svelte +23 -0
  40. package/dist/ui/internal/checkSocket.ts +48 -0
  41. package/dist/ui/internal/queryEngine.ts +9 -2
  42. package/dist/ui/internal/telemetry.ts +1 -0
  43. package/dist/ui/web.js +5 -55
  44. package/package.json +9 -10
  45. package/cli.ts +0 -156
  46. package/dist/ui/internal/NavSidebarHMR.svelte +0 -8
@@ -1,116 +1,72 @@
1
- <script context="module">
1
+ <script lang="ts" module>
2
2
  export const evidenceInclude = true
3
3
  </script>
4
4
 
5
5
  <script lang="ts">
6
- import {getContext, onDestroy} from 'svelte'
6
+ import {getContext, onDestroy, onMount, untrack} from 'svelte'
7
+ import {type Writable, get} from 'svelte/store'
7
8
  import {propKey, strictBuild} from '../component-utilities/chartContext.js'
8
9
  import {getThemeStores} from '../component-utilities/themeStores'
9
10
  import {toBoolean} from '../component-utilities/convert'
10
11
  import {parseCommaList} from '../component-utilities/inputUtils.ts'
11
12
 
12
- export let id: string
13
- export let description: string | undefined = undefined
14
- export let contentType: string | undefined = undefined
15
- export let title: string | undefined = undefined
16
- export let align: string | undefined = undefined
17
- export let wrap: boolean | string | undefined = undefined
18
- export let wrapTitle: boolean | string | undefined = undefined
19
- export let height: string | undefined = undefined
20
- export let width: string | undefined = undefined
21
- export let alt: string | undefined = undefined
22
- export let openInNewTab: boolean | string | undefined = undefined
23
- export let linkLabel: string | undefined = undefined
24
- export let fmt: string | undefined = undefined
25
- export let totalAgg: string | undefined = undefined
26
- export let totalFmt: string | undefined = undefined
27
- export let weightCol: string | undefined = undefined
28
- export let subtotalFmt: string | undefined = undefined
29
- export let colorMax: string | undefined = undefined
30
- export let colorMin: string | undefined = undefined
31
- export let colorMid: string | undefined = undefined
32
- export let colorBreakpoints: string[] | undefined = undefined
33
- export let colorScale: any = 'default'
34
- export let scaleColumn: string | undefined = undefined
35
- export let downIsGood: boolean | string | undefined = undefined
36
- export let showValue: boolean | string | undefined = undefined
37
- export let deltaSymbol: boolean | string | undefined = undefined
38
- export let neutralMin: number | string | undefined = 0
39
- export let neutralMax: number | string | undefined = 0
40
- export let chip: boolean | string | undefined = undefined
41
- export let sparkWidth: number | string | undefined = undefined
42
- export let sparkHeight: number | string | undefined = undefined
43
- export let sparkColor: string | undefined = undefined
44
- export let sparkX: string | undefined = undefined
45
- export let sparkY: string | undefined = undefined
46
- export let sparkYScale: boolean | string | undefined = undefined
47
- export let barColor: string | undefined = '#a5cdee'
48
- export let negativeBarColor: string | undefined = '#fca5a5'
49
- export let backgroundColor: string | undefined = 'transparent'
50
- export let hideLabels: boolean | string | undefined = undefined
51
- export let colGroup: string | undefined = undefined
52
- export let fmtColumn: string | undefined = undefined
53
- export let redNegatives: boolean | string | undefined = undefined
13
+ interface Props {
14
+ id: string, description?: string, contentType?: string, title?: string, align?: string
15
+ wrap?: boolean | string, wrapTitle?: boolean | string, height?: string, width?: string, alt?: string
16
+ openInNewTab?: boolean | string, linkLabel?: string, fmt?: string, totalAgg?: string, totalFmt?: string
17
+ weightCol?: string, subtotalFmt?: string, colorMax?: string, colorMin?: string, colorMid?: string
18
+ colorBreakpoints?: string[], colorScale?: any, scaleColumn?: string, downIsGood?: boolean | string
19
+ showValue?: boolean | string, deltaSymbol?: boolean | string, neutralMin?: number | string
20
+ neutralMax?: number | string, chip?: boolean | string, sparkWidth?: number | string
21
+ sparkHeight?: number | string, sparkColor?: string, sparkX?: string, sparkY?: string
22
+ sparkYScale?: boolean | string, barColor?: string, negativeBarColor?: string, backgroundColor?: string
23
+ hideLabels?: boolean | string, colGroup?: string, fmtColumn?: string, redNegatives?: boolean | string
24
+ }
54
25
 
55
- const {resolveColor, resolveColorPalette} = getThemeStores()
26
+ let {
27
+ id, description = undefined, contentType = undefined, title = undefined, align = undefined,
28
+ wrap = undefined, wrapTitle = undefined, height = undefined, width = undefined, alt = undefined,
29
+ openInNewTab = undefined, linkLabel = undefined, fmt = undefined, totalAgg = undefined,
30
+ totalFmt = undefined, weightCol = undefined, subtotalFmt = undefined, colorMax = undefined,
31
+ colorMin = undefined, colorMid = undefined, colorBreakpoints = undefined, colorScale = 'default',
32
+ scaleColumn = undefined, downIsGood = undefined, showValue = undefined, deltaSymbol = undefined,
33
+ neutralMin = 0, neutralMax = 0, chip = undefined, sparkWidth = undefined, sparkHeight = undefined,
34
+ sparkColor = undefined, sparkX = undefined, sparkY = undefined, sparkYScale = undefined,
35
+ barColor = '#a5cdee', negativeBarColor = '#fca5a5', backgroundColor = 'transparent',
36
+ hideLabels = undefined, colGroup = undefined, fmtColumn = undefined, redNegatives = undefined,
37
+ }: Props = $props()
56
38
 
57
- let barColorStore = resolveColor(barColor)
58
- let negativeBarColorStore = resolveColor(negativeBarColor)
59
- let backgroundColorStore = resolveColor(backgroundColor)
60
- let colorScaleStore = resolveColorPalette(colorScale)
39
+ const {resolveColor, resolveColorPalette} = getThemeStores()
61
40
 
62
- $: barColorStore = resolveColor(barColor)
63
- $: negativeBarColorStore = resolveColor(negativeBarColor)
64
- $: backgroundColorStore = resolveColor(backgroundColor)
65
- $: colorScaleStore = resolveColorPalette(colorScale)
41
+ // Get stores reactively - use $derived to track prop changes
42
+ let barColorStore = $derived(resolveColor(barColor))
43
+ let negativeBarColorStore = $derived(resolveColor(negativeBarColor))
44
+ let backgroundColorStore = $derived(resolveColor(backgroundColor))
45
+ let colorScaleStore = $derived(resolveColorPalette(colorScale))
66
46
 
67
- const props = getContext(propKey)
68
- $: colorBreakpoints = parseCommaList(colorBreakpoints)
47
+ const chartProps = getContext<Writable<any>>(propKey)
69
48
 
70
49
  const identifier = Symbol('GrapheneColumn')
71
50
 
72
- wrap = toBoolean(wrap) ?? false
73
- wrapTitle = toBoolean(wrapTitle) ?? false
74
- openInNewTab = toBoolean(openInNewTab) ?? false
75
- downIsGood = toBoolean(downIsGood) ?? false
76
- showValue = toBoolean(showValue) ?? true
77
- deltaSymbol = toBoolean(deltaSymbol) ?? true
78
- chip = toBoolean(chip) ?? false
79
- sparkYScale = toBoolean(sparkYScale) ?? false
80
- hideLabels = toBoolean(hideLabels) ?? false
81
- redNegatives = toBoolean(redNegatives) ?? false
82
-
83
51
  const coerceNumber = (value: number | string | undefined): number | undefined => {
84
52
  if (value === undefined || value === null || value === '') return undefined
85
53
  let parsed = Number(value)
86
54
  return Number.isNaN(parsed) ? undefined : parsed
87
55
  }
88
56
 
89
- const checkColumnName = () => {
90
- try {
91
- let data = $props.data?.[0]
92
- if (!data || !Object.keys(data).includes(id)) {
93
- let error = `Error in table: ${id} does not exist in the dataset`
94
- if (strictBuild) throw new Error(error)
95
- console.warn(error)
96
- }
97
- } catch (error) {
98
- if (strictBuild) throw error
99
- }
100
- }
101
-
102
- const options = () => ({
57
+ // Build the column options object - as a function so it can be called synchronously
58
+ const getColumnOptions = () => ({
103
59
  identifier,
104
60
  id,
105
61
  title,
106
62
  align,
107
- wrap,
108
- wrapTitle,
63
+ wrap: toBoolean(wrap) ?? false,
64
+ wrapTitle: toBoolean(wrapTitle) ?? false,
109
65
  contentType,
110
66
  height,
111
67
  width,
112
68
  alt,
113
- openInNewTab,
69
+ openInNewTab: toBoolean(openInNewTab) ?? false,
114
70
  linkLabel,
115
71
  fmt,
116
72
  fmtColumn,
@@ -118,57 +74,90 @@
118
74
  totalFmt,
119
75
  subtotalFmt,
120
76
  weightCol,
121
- downIsGood,
122
- deltaSymbol,
123
- chip,
77
+ downIsGood: toBoolean(downIsGood) ?? false,
78
+ deltaSymbol: toBoolean(deltaSymbol) ?? true,
79
+ chip: toBoolean(chip) ?? false,
124
80
  neutralMin: coerceNumber(neutralMin) ?? 0,
125
81
  neutralMax: coerceNumber(neutralMax) ?? 0,
126
- showValue,
82
+ showValue: toBoolean(showValue) ?? true,
127
83
  colorMax,
128
84
  colorMin,
129
85
  colorMid,
130
- colorScale: $colorScaleStore,
131
- colorBreakpoints,
86
+ colorScale: get(colorScaleStore),
87
+ colorBreakpoints: parseCommaList(colorBreakpoints),
132
88
  scaleColumn,
133
89
  colGroup,
134
90
  description,
135
- redNegatives,
91
+ redNegatives: toBoolean(redNegatives) ?? false,
136
92
  sparkWidth,
137
93
  sparkHeight,
138
94
  sparkColor,
139
95
  sparkX,
140
96
  sparkY,
141
- sparkYScale,
142
- barColor: $barColorStore,
143
- negativeBarColor: $negativeBarColorStore,
144
- backgroundColor: $backgroundColorStore,
145
- hideLabels,
97
+ sparkYScale: toBoolean(sparkYScale) ?? false,
98
+ barColor: get(barColorStore),
99
+ negativeBarColor: get(negativeBarColorStore),
100
+ backgroundColor: get(backgroundColorStore),
101
+ hideLabels: toBoolean(hideLabels) ?? false,
146
102
  })
147
103
 
148
- const updateProps = () => {
149
- checkColumnName()
150
- props.update((state: any) => {
151
- let next = {...state}
152
- let existing = next.columns.findIndex((column: any) => column.identifier === identifier)
153
- let option = options()
154
- if (existing === -1) {
155
- next.columns = [...next.columns, option]
156
- } else {
157
- next.columns = [
158
- ...next.columns.slice(0, existing),
159
- option,
160
- ...next.columns.slice(existing + 1),
161
- ]
104
+ // Register column on mount
105
+ onMount(() => {
106
+ // Check column name once on mount (not reactively)
107
+ try {
108
+ let data = get(chartProps).data?.[0]
109
+ if (data && !Object.keys(data).includes(id)) {
110
+ let error = `Error in table: ${id} does not exist in the dataset`
111
+ if (strictBuild) throw new Error(error)
112
+ console.warn(error)
162
113
  }
114
+ } catch (error) {
115
+ if (strictBuild) throw error
116
+ }
117
+
118
+ // Initial registration
119
+ chartProps.update((state: any) => {
120
+ let next = {...state, columns: [...state.columns, getColumnOptions()]}
163
121
  return next
164
122
  })
165
- }
123
+ })
166
124
 
167
- $: updateProps()
125
+ // Update column options when props change
126
+ // Track all the props that affect columnOptions
127
+ $effect(() => {
128
+ // Read all props that could change
129
+ void [id, title, align, wrap, wrapTitle, contentType, height, width, alt, openInNewTab,
130
+ linkLabel, fmt, fmtColumn, totalAgg, totalFmt, subtotalFmt, weightCol, downIsGood,
131
+ deltaSymbol, chip, neutralMin, neutralMax, showValue, colorMax, colorMin, colorMid,
132
+ colorScale, colorBreakpoints, scaleColumn, colGroup, description, redNegatives,
133
+ sparkWidth, sparkHeight, sparkColor, sparkX, sparkY, sparkYScale, barColor,
134
+ negativeBarColor, backgroundColor, hideLabels]
135
+ // Also track store values
136
+ void [$colorScaleStore, $barColorStore, $negativeBarColorStore, $backgroundColorStore]
137
+
138
+ // Use untrack to prevent this update from creating a dependency loop
139
+ untrack(() => {
140
+ chartProps.update((state: any) => {
141
+ let next = {...state}
142
+ let existing = next.columns.findIndex((column: any) => column.identifier === identifier)
143
+ let option = getColumnOptions()
144
+ if (existing !== -1) {
145
+ next.columns = [
146
+ ...next.columns.slice(0, existing),
147
+ option,
148
+ ...next.columns.slice(existing + 1),
149
+ ]
150
+ }
151
+ return next
152
+ })
153
+ })
154
+ })
168
155
 
169
156
  onDestroy(() => {
170
- props.update((state: any) => {
171
- return {...state, columns: state.columns.filter((column: any) => column.identifier !== identifier)}
157
+ untrack(() => {
158
+ chartProps.update((state: any) => {
159
+ return {...state, columns: state.columns.filter((column: any) => column.identifier !== identifier)}
160
+ })
172
161
  })
173
162
  })
174
163
  </script>
@@ -2,17 +2,25 @@
2
2
  import {onMount} from 'svelte'
3
3
  import {toBoolean} from '../component-utilities/inputUtils'
4
4
 
5
- export let name: string
6
- export let label: string | undefined = undefined
7
- export let title: string | undefined = undefined
8
- export let description: string | undefined = undefined
9
- export let start: string | Date | undefined = undefined
10
- export let end: string | Date | undefined = undefined
11
- export let defaultValue: string | undefined = undefined
12
- export let presetRanges: string | string[] | undefined = undefined
13
- export let data: string | undefined = undefined
14
- export let dates: string | undefined = undefined
15
- export let hideDuringPrint: boolean | string = true
5
+ interface Props {
6
+ name: string
7
+ label?: string
8
+ title?: string
9
+ description?: string
10
+ start?: string | Date
11
+ end?: string | Date
12
+ defaultValue?: string
13
+ presetRanges?: string | string[]
14
+ data?: string
15
+ dates?: string
16
+ hideDuringPrint?: boolean | string
17
+ }
18
+
19
+ let {
20
+ name, label = undefined, title = undefined, description = undefined, start = undefined,
21
+ end = undefined, defaultValue = undefined, presetRanges = undefined, data = undefined,
22
+ dates = undefined, hideDuringPrint = true,
23
+ }: Props = $props()
16
24
 
17
25
  const DEFAULT_PRESETS = ['Last 7 Days', 'Last 30 Days', 'Last 90 Days', 'Last 365 Days', 'Last Month', 'Last Year', 'Month to Date', 'Month to Today', 'Year to Date', 'Year to Today', 'All Time']
18
26
 
@@ -20,21 +28,21 @@
20
28
  let queryKey = ''
21
29
  let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
22
30
 
23
- let domainStart: string | null = null
24
- let domainEnd: string | null = null
31
+ let domainStart: string | null = $state(null)
32
+ let domainEnd: string | null = $state(null)
25
33
 
26
- let currentStart: string | null = null
27
- let currentEnd: string | null = null
28
- let currentPreset: string = ''
34
+ let currentStart: string | null = $state(null)
35
+ let currentEnd: string | null = $state(null)
36
+ let currentPreset: string = $state('')
29
37
  let touched = false
30
38
 
31
- $: hidePrint = toBoolean(hideDuringPrint)
32
- $: presetList = (() => {
39
+ let hidePrint = $derived(toBoolean(hideDuringPrint))
40
+ let presetList = $derived((() => {
33
41
  if (Array.isArray(presetRanges)) return presetRanges
34
42
  if (presetRanges) return [presetRanges]
35
43
  return DEFAULT_PRESETS
36
- })()
37
- $: displayLabel = title || label
44
+ })())
45
+ let displayLabel = $derived(title || label)
38
46
 
39
47
  onMount(() => {
40
48
  mounted = true
@@ -55,7 +63,9 @@
55
63
  }
56
64
  })
57
65
 
58
- $: refreshQuery()
66
+ $effect(() => {
67
+ refreshQuery()
68
+ })
59
69
 
60
70
  function refreshQuery () {
61
71
  if (!mounted) return
@@ -108,7 +118,7 @@
108
118
  }
109
119
 
110
120
  function addDays (value: Date, days: number): Date {
111
- let copy = new Date(value)
121
+ let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
112
122
  copy.setDate(copy.getDate() + days)
113
123
  return copy
114
124
  }
@@ -128,13 +138,13 @@
128
138
  }
129
139
 
130
140
  function addMonths (value: Date, months: number): Date {
131
- let copy = new Date(value)
141
+ let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
132
142
  copy.setMonth(copy.getMonth() + months)
133
143
  return copy
134
144
  }
135
145
 
136
146
  function addYears (value: Date, years: number): Date {
137
- let copy = new Date(value)
147
+ let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
138
148
  copy.setFullYear(copy.getFullYear() + years)
139
149
  return copy
140
150
  }
@@ -263,12 +273,12 @@
263
273
  <div class="input-description">{description}</div>
264
274
  {/if}
265
275
  <div class="range-row">
266
- <input id={`daterange-${name}-start`} class="date-input" type="date" value={currentStart || ''} on:change={onStartChange} />
276
+ <input id={`daterange-${name}-start`} class="date-input" type="date" value={currentStart || ''} onchange={onStartChange} />
267
277
  <span class="range-separator">to</span>
268
- <input id={`daterange-${name}-end`} class="date-input" type="date" value={currentEnd || ''} on:change={onEndChange} />
278
+ <input id={`daterange-${name}-end`} class="date-input" type="date" value={currentEnd || ''} onchange={onEndChange} />
269
279
  </div>
270
280
  {#if presetList.length}
271
- <select class="preset-select" on:change={onPresetChange}>
281
+ <select class="preset-select" onchange={onPresetChange}>
272
282
  <option value="">Custom range</option>
273
283
  {#each presetList as preset (preset)}
274
284
  <option value={preset} selected={preset === currentPreset}>{preset}</option>
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import {onMount, setContext, tick} from 'svelte'
2
+ import {onMount, setContext, tick, type Snippet} from 'svelte'
3
3
  import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
4
4
  import {ensureArray, toBoolean} from '../component-utilities/inputUtils'
5
5
 
@@ -8,38 +8,48 @@
8
8
  label: string
9
9
  }
10
10
 
11
- export let name: string
12
- export let data: string | undefined = undefined
13
- export let value: string = 'value'
14
- export let label: string | undefined = undefined
15
- export let optionLabel: string | undefined = undefined
16
- export let labelField: string | undefined = undefined
17
- export let title: string | undefined = undefined
18
- export let placeholder: string = 'Select option'
19
- export let multiple: boolean | string = false
20
- export let defaultValue: string | string[] | undefined = undefined
21
- export let selectAllByDefault: boolean | string = false
22
- export let noDefault: boolean | string = false
23
- export let disableSelectAll: boolean | string = false
24
- export let hideDuringPrint: boolean | string = true
25
- export let description: string | undefined = undefined
26
- export let disabled: boolean | string = false
11
+ interface Props {
12
+ name: string
13
+ data?: string
14
+ value?: string
15
+ label?: string
16
+ optionLabel?: string
17
+ labelField?: string
18
+ title?: string
19
+ placeholder?: string
20
+ multiple?: boolean | string
21
+ defaultValue?: string | string[]
22
+ selectAllByDefault?: boolean | string
23
+ noDefault?: boolean | string
24
+ disableSelectAll?: boolean | string
25
+ hideDuringPrint?: boolean | string
26
+ description?: string
27
+ disabled?: boolean | string
28
+ children?: Snippet
29
+ }
30
+
31
+ let {
32
+ name, data = undefined, value = 'value', label = undefined, optionLabel = undefined,
33
+ labelField = undefined, title = undefined, placeholder = 'Select option', multiple = false,
34
+ defaultValue = undefined, selectAllByDefault = false, noDefault = false, disableSelectAll = false,
35
+ hideDuringPrint = true, description = undefined, disabled = false, children = undefined,
36
+ }: Props = $props()
27
37
 
28
38
  let mounted = false
29
- let queryOptions: Option[] = []
30
- let manualOptions: Option[] = []
31
- let selection: any[] = []
39
+ let queryOptions: Option[] = $state([])
40
+ let manualOptions: Option[] = $state([])
41
+ let selection: any[] = $state([])
32
42
  let touched = false
33
43
  let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
34
44
  let queryKey = ''
35
45
 
36
- let isOpen = false
37
- let searchTerm = ''
38
- let activeIndex = -1
39
- let triggerEl: HTMLButtonElement | null = null
40
- let menuEl: HTMLDivElement | null = null
41
- let searchInput: HTMLInputElement | null = null
42
- let triggerWidth = 0
46
+ let isOpen = $state(false)
47
+ let searchTerm = $state('')
48
+ let activeIndex = $state(-1)
49
+ let triggerEl: HTMLButtonElement | null = $state(null)
50
+ let menuEl: HTMLDivElement | null = $state(null)
51
+ let searchInput: HTMLInputElement | null = $state(null)
52
+ let triggerWidth = $state(0)
43
53
 
44
54
  const registerOption = (opt: Option) => {
45
55
  manualOptions = [...manualOptions, opt]
@@ -65,7 +75,7 @@
65
75
  }
66
76
 
67
77
  const combineOptions = (manual: Option[], queried: Option[]) => {
68
- let map = new Map<string, Option>()
78
+ let map = new Map<string, Option>() // eslint-disable-line svelte/prefer-svelte-reactivity
69
79
  for (let opt of [...manual, ...queried]) {
70
80
  let key = optionKey(opt.value)
71
81
  if (!map.has(key)) map.set(key, opt)
@@ -73,23 +83,26 @@
73
83
  return Array.from(map.values())
74
84
  }
75
85
 
76
- $: multi = toBoolean(multiple)
77
- $: selectAllDefault = toBoolean(selectAllByDefault)
78
- $: hasNoDefault = toBoolean(noDefault)
79
- $: hidePrint = toBoolean(hideDuringPrint)
80
- $: isDisabled = toBoolean(disabled)
81
- $: disableSelectAllButton = toBoolean(disableSelectAll)
86
+ let multi = $derived(toBoolean(multiple))
87
+ let selectAllDefault = $derived(toBoolean(selectAllByDefault))
88
+ let hasNoDefault = $derived(toBoolean(noDefault))
89
+ let hidePrint = $derived(toBoolean(hideDuringPrint))
90
+ let isDisabled = $derived(toBoolean(disabled))
91
+ let disableSelectAllButton = $derived(toBoolean(disableSelectAll))
82
92
 
83
- $: resolvedLabelField = optionLabel || labelField || (label && data ? label : undefined)
84
- $: resolvedTitle = title || (!data ? label : undefined)
85
- $: triggerPlaceholder = placeholder || resolvedTitle || 'Select option'
86
- $: searchPlaceholder = resolvedTitle || placeholder || 'Search options'
93
+ let resolvedLabelField = $derived(optionLabel || labelField || (label && data ? label : undefined))
94
+ let resolvedTitle = $derived(title || (!data ? label : undefined))
95
+ let triggerPlaceholder = $derived(placeholder || resolvedTitle || 'Select option')
96
+ let searchPlaceholder = $derived(resolvedTitle || placeholder || 'Search options')
87
97
 
88
- $: availableOptions = combineOptions(manualOptions, queryOptions)
89
- $: valueMap = new Map(availableOptions.map(opt => [optionKey(opt.value), opt]))
90
- $: filteredOptions = filterOptions(availableOptions, searchTerm)
91
- $: if (isOpen) activeIndex = ensureActiveIndex(activeIndex, filteredOptions)
92
- $: selectedDisplayOptions = selection.map(val => valueMap.get(optionKey(val)) || {value: val, label: String(val ?? '')})
98
+ let availableOptions = $derived(combineOptions(manualOptions, queryOptions))
99
+ let valueMap = $derived(new Map(availableOptions.map(opt => [optionKey(opt.value), opt])))
100
+ let filteredOptions = $derived(filterOptions(availableOptions, searchTerm))
101
+ let selectedDisplayOptions = $derived(selection.map(val => valueMap.get(optionKey(val)) || {value: val, label: String(val ?? '')}))
102
+
103
+ $effect(() => {
104
+ if (isOpen) activeIndex = ensureActiveIndex(activeIndex, filteredOptions)
105
+ })
93
106
 
94
107
  function setupQuery () {
95
108
  if (!mounted) return
@@ -276,7 +289,9 @@
276
289
  optionEl?.scrollIntoView({block: 'nearest'})
277
290
  }
278
291
 
279
- $: if (isOpen && activeIndex >= 0) tick().then(scrollActiveIntoView)
292
+ $effect(() => {
293
+ if (isOpen && activeIndex >= 0) tick().then(scrollActiveIntoView)
294
+ })
280
295
 
281
296
  onMount(() => {
282
297
  mounted = true
@@ -303,9 +318,13 @@
303
318
  }
304
319
  })
305
320
 
306
- $: setupQuery()
321
+ $effect(() => {
322
+ setupQuery()
323
+ })
307
324
 
308
- $: if (triggerEl) updateTriggerWidth()
325
+ $effect(() => {
326
+ if (triggerEl) updateTriggerWidth()
327
+ })
309
328
 
310
329
  function syncSelection (fromUser: boolean) {
311
330
  let opts = availableOptions
@@ -356,8 +375,8 @@
356
375
  setSelection([], true)
357
376
  }
358
377
 
359
- const elementId = `dropdown-${name}`
360
- const menuId = `${elementId}-menu`
378
+ let elementId = $derived(`dropdown-${name}`)
379
+ let menuId = $derived(`${elementId}-menu`)
361
380
 
362
381
  function getContainerClass () {
363
382
  if (!hidePrint) return 'input-block'
@@ -377,6 +396,7 @@
377
396
  }
378
397
  </script>
379
398
 
399
+ {@render children?.()}
380
400
  <div class={getContainerClass()}>
381
401
  {#if resolvedTitle}
382
402
  <label class="input-label" for={elementId}>{resolvedTitle}</label>
@@ -397,8 +417,8 @@
397
417
  aria-expanded={isOpen}
398
418
  aria-controls={menuId}
399
419
  aria-activedescendant={isOpen && activeIndex >= 0 ? `${menuId}-option-${activeIndex}` : undefined}
400
- on:click={toggleMenu}
401
- on:keydown={handleTriggerKeydown}
420
+ onclick={toggleMenu}
421
+ onkeydown={handleTriggerKeydown}
402
422
  >
403
423
  <span class="dropdown-trigger-label">
404
424
  {#if multi}
@@ -432,7 +452,7 @@
432
452
  tabindex="0"
433
453
  aria-multiselectable={multi}
434
454
  style={`min-width: ${Math.max(triggerWidth, 220)}px`}
435
- on:keydown={handleMenuKeydown}
455
+ onkeydown={handleMenuKeydown}
436
456
  >
437
457
  <div class="dropdown-search">
438
458
  <input
@@ -455,10 +475,10 @@
455
475
  aria-selected={isOptionSelected(opt)}
456
476
  data-index={index}
457
477
  id={`${menuId}-option-${index}`}
458
- on:mousedown|preventDefault
459
- on:mouseenter={() => activeIndex = index}
460
- on:click={() => handleOptionSelect(opt, false)}
461
- on:keydown={(event) => handleOptionKeydown(event, opt)}
478
+ onmousedown={(e) => e.preventDefault()}
479
+ onmouseenter={() => activeIndex = index}
480
+ onclick={() => handleOptionSelect(opt, false)}
481
+ onkeydown={(event) => handleOptionKeydown(event, opt)}
462
482
  >
463
483
  <span class="dropdown-check" aria-hidden="true">
464
484
  {#if multi}
@@ -501,13 +521,13 @@
501
521
  {#if multi}
502
522
  <div class="dropdown-footer">
503
523
  {#if !disableSelectAllButton}
504
- <button type="button" class="dropdown-footer-action" on:click|stopPropagation={(event) => { event.preventDefault(); selectAll() }}>Select all</button>
524
+ <button type="button" class="dropdown-footer-action" onclick={(event) => { event.stopPropagation(); event.preventDefault(); selectAll() }}>Select all</button>
505
525
  {/if}
506
526
  <button
507
527
  type="button"
508
528
  class="dropdown-footer-action"
509
529
  disabled={!selection.length}
510
- on:click|stopPropagation={(event) => { event.preventDefault(); clearSelection() }}
530
+ onclick={(event) => { event.stopPropagation(); event.preventDefault(); clearSelection() }}
511
531
  >
512
532
  Clear selection
513
533
  </button>
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
- import {getContext, onDestroy, onMount} from 'svelte'
2
+ import {getContext, onMount} from 'svelte'
3
3
  import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
4
4
 
5
- export let value: any
6
- export let valueLabel: string | undefined = undefined
5
+ interface Props {
6
+ value: any
7
+ valueLabel?: string
8
+ }
9
+
10
+ let {value, valueLabel = undefined}: Props = $props()
7
11
 
8
12
  type RegisterFn = ((option: {value: any; label: string}) => (() => void) | void) | undefined
9
13
  const register = getContext<RegisterFn>(DROPDOWN_CONTEXT)
@@ -13,9 +17,8 @@
13
17
  onMount(() => {
14
18
  if (!register) return
15
19
  unregister = register({value, label: valueLabel ?? String(value)})
16
- })
17
-
18
- onDestroy(() => {
19
- if (typeof unregister === 'function') unregister()
20
+ return () => {
21
+ if (typeof unregister === 'function') unregister()
22
+ }
20
23
  })
21
24
  </script>