@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,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import {onMount} from 'svelte'
3
3
  import {toBoolean} from '../component-utilities/inputUtils'
4
+ import {captureInitial, getPageInputs} from '../internal/pageInputs.svelte.js'
4
5
 
5
6
  interface Props {
6
7
  name: string
@@ -27,13 +28,15 @@
27
28
  let mounted = false
28
29
  let queryKey = ''
29
30
  let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
31
+ let pageInputs = getPageInputs()
32
+ let field = captureInitial(() => pageInputs.dateRange(name))
30
33
 
31
34
  let domainStart: string | null = $state(null)
32
35
  let domainEnd: string | null = $state(null)
33
36
 
34
37
  let currentStart: string | null = $state(null)
35
38
  let currentEnd: string | null = $state(null)
36
- let currentPreset: string = $state('')
39
+ let currentPreset: string | null = $state(null)
37
40
  let touched = false
38
41
 
39
42
  let hidePrint = $derived(toBoolean(hideDuringPrint))
@@ -46,16 +49,16 @@
46
49
 
47
50
  onMount(() => {
48
51
  mounted = true
49
- currentStart = normalizeInput(start)
50
- currentEnd = normalizeInput(end)
51
- if (defaultValue && presetList.includes(defaultValue)) {
52
- applyPreset(defaultValue, false)
53
- } else {
54
- updateParams()
55
- }
52
+ currentStart = field.hasExternalValue ? field.value.start : normalizeInput(start)
53
+ currentEnd = field.hasExternalValue ? field.value.end : normalizeInput(end)
54
+ currentPreset = inferPreset(currentStart, currentEnd)
55
+ if (field.hasExternalValue) updateParams()
56
+ else if (defaultValue && presetList.includes(defaultValue)) applyPreset(defaultValue, false)
57
+ else updateParams()
56
58
  refreshQuery()
57
59
  return () => {
58
60
  mounted = false
61
+ field.destroy()
59
62
  if (queryHandler) {
60
63
  window.$GRAPHENE?.unsubscribe?.(queryHandler)
61
64
  queryHandler = null
@@ -67,7 +70,13 @@
67
70
  refreshQuery()
68
71
  })
69
72
 
70
- function refreshQuery () {
73
+ $effect(() => {
74
+ if (currentStart === field.value.start && currentEnd === field.value.end) return
75
+ if (!mounted) return
76
+ setRange(field.value.start, field.value.end, inferPreset(field.value.start, field.value.end), {persist: false})
77
+ })
78
+
79
+ function refreshQuery() {
71
80
  if (!mounted) return
72
81
  let key = data && dates ? `${data}::${dates}` : ''
73
82
  if (key === queryKey) return
@@ -78,7 +87,7 @@
78
87
  queryKey = key
79
88
  if (!data || !dates) return
80
89
  let handler = (res: {rows?: any[]; error?: any}) => {
81
- if (res.error || !res.rows?.length) return
90
+ if (!res || res.error || !res.rows?.length) return
82
91
  let values = res.rows
83
92
  .map(row => normalizeInput(row[dates]))
84
93
  .filter((val): val is string => !!val)
@@ -86,13 +95,15 @@
86
95
  values.sort()
87
96
  domainStart = values[0]
88
97
  domainEnd = values[values.length - 1]
89
- if (!touched) {
98
+ if (field.hasExternalValue) {
99
+ currentPreset = inferPreset(currentStart, currentEnd)
100
+ } else if (!touched) {
90
101
  if (defaultValue && presetList.includes(defaultValue)) {
91
102
  applyPreset(defaultValue, false)
92
103
  } else {
93
104
  let startCandidate = currentStart ?? domainStart
94
105
  let endCandidate = currentEnd ?? (domainEnd ? addDaysString(domainEnd, 1) : null)
95
- setRange(startCandidate, endCandidate, currentPreset, false)
106
+ setRange(startCandidate, endCandidate, currentPreset, {markTouched: false, persist: true})
96
107
  }
97
108
  }
98
109
  }
@@ -102,7 +113,7 @@
102
113
  }
103
114
  }
104
115
 
105
- function normalizeInput (value: string | Date | null | undefined): string | null {
116
+ function normalizeInput(value: string | Date | null | undefined): string | null {
106
117
  if (value === null || value === undefined) return null
107
118
  if (value instanceof Date) return formatDate(value)
108
119
  let parsed = new Date(value)
@@ -110,69 +121,90 @@
110
121
  return formatDate(parsed)
111
122
  }
112
123
 
113
- function formatDate (value: Date): string {
124
+ function formatDate(value: Date): string {
114
125
  let year = value.getFullYear()
115
126
  let month = String(value.getMonth() + 1).padStart(2, '0')
116
127
  let day = String(value.getDate()).padStart(2, '0')
117
128
  return `${year}-${month}-${day}`
118
129
  }
119
130
 
120
- function addDays (value: Date, days: number): Date {
131
+ function addDays(value: Date, days: number): Date {
121
132
  let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
122
133
  copy.setDate(copy.getDate() + days)
123
134
  return copy
124
135
  }
125
136
 
126
- function addDaysString (value: string, days: number): string {
137
+ function addDaysString(value: string, days: number): string {
127
138
  let parsed = new Date(value)
128
139
  if (Number.isNaN(parsed.getTime())) return value
129
140
  return formatDate(addDays(parsed, days))
130
141
  }
131
142
 
132
- function startOfMonth (value: Date): Date {
143
+ function startOfMonth(value: Date): Date {
133
144
  return new Date(value.getFullYear(), value.getMonth(), 1)
134
145
  }
135
146
 
136
- function startOfYear (value: Date): Date {
147
+ function startOfYear(value: Date): Date {
137
148
  return new Date(value.getFullYear(), 0, 1)
138
149
  }
139
150
 
140
- function addMonths (value: Date, months: number): Date {
151
+ function addMonths(value: Date, months: number): Date {
141
152
  let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
142
153
  copy.setMonth(copy.getMonth() + months)
143
154
  return copy
144
155
  }
145
156
 
146
- function addYears (value: Date, years: number): Date {
157
+ function addYears(value: Date, years: number): Date {
147
158
  let copy = new Date(value) // eslint-disable-line svelte/prefer-svelte-reactivity
148
159
  copy.setFullYear(copy.getFullYear() + years)
149
160
  return copy
150
161
  }
151
162
 
152
- function setRange (startValue: string | null, endValue: string | null, preset: string, markTouched: boolean) {
163
+ // We only persist start/end in URL state, so the preset label is inferred by matching
164
+ // the current range against the configured preset definitions.
165
+ function inferPreset(startValue: string | null, endValue: string | null): string | null {
166
+ if (!startValue && !endValue) return null
167
+ let baseEnd = (() => {
168
+ if (endValue) {
169
+ let parsed = new Date(endValue)
170
+ if (!Number.isNaN(parsed.getTime())) return addDays(parsed, -1)
171
+ }
172
+ if (domainEnd) return new Date(domainEnd)
173
+ return new Date()
174
+ })()
175
+ if (Number.isNaN(baseEnd.getTime())) return null
176
+ for (let preset of presetList) {
177
+ let range = computePresetRange(preset, baseEnd)
178
+ let presetStart = range?.start ? formatDate(range.start) : null
179
+ let presetEnd = range?.end ? formatDate(range.end) : null
180
+ if (presetStart === startValue && presetEnd === endValue) return preset
181
+ }
182
+ return null
183
+ }
184
+
185
+ function setRange(startValue: string | null, endValue: string | null, preset: string | null, {markTouched = false, persist = true}: {markTouched?: boolean; persist?: boolean} = {}) {
153
186
  currentStart = startValue
154
187
  currentEnd = endValue
155
188
  currentPreset = preset
156
189
  if (markTouched) touched = true
157
- updateParams()
190
+ if (persist) updateParams()
158
191
  }
159
192
 
160
- function updateParams () {
161
- window.$GRAPHENE.updateParam(`${name}_start`, currentStart)
162
- window.$GRAPHENE.updateParam(`${name}_end`, currentEnd)
193
+ function updateParams() {
194
+ field.set({start: currentStart, end: currentEnd})
163
195
  }
164
196
 
165
- function onStartChange (event: Event) {
197
+ function onStartChange(event: Event) {
166
198
  let value = (event.currentTarget as HTMLInputElement).value || null
167
- setRange(value, currentEnd, '', true)
199
+ setRange(value, currentEnd, null, {markTouched: true, persist: true})
168
200
  }
169
201
 
170
- function onEndChange (event: Event) {
202
+ function onEndChange(event: Event) {
171
203
  let value = (event.currentTarget as HTMLInputElement).value || null
172
- setRange(currentStart, value, '', true)
204
+ setRange(currentStart, value, null, {markTouched: true, persist: true})
173
205
  }
174
206
 
175
- function applyPreset (preset: string, markTouched = true) {
207
+ function applyPreset(preset: string, markTouched = true) {
176
208
  let baseEnd = (() => {
177
209
  if (currentEnd) return new Date(currentEnd)
178
210
  if (domainEnd) return new Date(domainEnd)
@@ -183,10 +215,10 @@
183
215
  if (!range) return
184
216
  let startVal = range.start ? formatDate(range.start) : null
185
217
  let endVal = range.end ? formatDate(range.end) : null
186
- setRange(startVal, endVal, preset, markTouched)
218
+ setRange(startVal, endVal, preset, {markTouched, persist: true})
187
219
  }
188
220
 
189
- function computePresetRange (preset: string, baseEndInclusive: Date): {start: Date | null; end: Date | null} | null {
221
+ function computePresetRange(preset: string, baseEndInclusive: Date): {start: Date | null; end: Date | null} | null {
190
222
  let label = preset.trim()
191
223
  let today = new Date()
192
224
  let endExclusive = addDays(baseEndInclusive, 1)
@@ -254,10 +286,10 @@
254
286
  return null
255
287
  }
256
288
 
257
- function onPresetChange (event: Event) {
289
+ function onPresetChange(event: Event) {
258
290
  let value = (event.currentTarget as HTMLSelectElement).value
259
291
  if (!value) {
260
- currentPreset = ''
292
+ currentPreset = null
261
293
  touched = true
262
294
  return
263
295
  }
@@ -300,6 +332,7 @@
300
332
  }
301
333
  }
302
334
  .input-label {
335
+ font-family: var(--font-ui);
303
336
  font-size: 12px;
304
337
  font-weight: 600;
305
338
  color: var(--input-label-color, #374151);
@@ -323,6 +356,8 @@
323
356
  border: 1px solid rgba(107, 114, 128, 0.4);
324
357
  font-size: 14px;
325
358
  min-width: 150px;
359
+ font-family: var(--font-ui);
360
+ font-synthesis: none;
326
361
  }
327
362
  .preset-select {
328
363
  max-width: 220px;
@@ -330,5 +365,7 @@
330
365
  border-radius: 6px;
331
366
  border: 1px solid rgba(107, 114, 128, 0.4);
332
367
  font-size: 13px;
368
+ font-family: var(--font-ui);
369
+ font-synthesis: none;
333
370
  }
334
371
  </style>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import {onMount, setContext, tick, type Snippet} from 'svelte'
3
- import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
4
3
  import {ensureArray, toBoolean} from '../component-utilities/inputUtils'
4
+ import {captureInitial, getPageInputs} from '../internal/pageInputs.svelte.js'
5
5
 
6
6
  interface Option {
7
7
  value: any
@@ -42,6 +42,8 @@
42
42
  let touched = false
43
43
  let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
44
44
  let queryKey = ''
45
+ let pageInputs = getPageInputs()
46
+ let field = captureInitial(() => pageInputs.dropdown(name, toBoolean(multiple)))
45
47
 
46
48
  let isOpen = $state(false)
47
49
  let searchTerm = $state('')
@@ -59,7 +61,7 @@
59
61
  syncSelection(false)
60
62
  }
61
63
  }
62
- setContext(DROPDOWN_CONTEXT, registerOption)
64
+ setContext('dropdown', registerOption)
63
65
 
64
66
  const optionKey = (val: any): string => {
65
67
  if (val === null) return 'null'
@@ -80,7 +82,7 @@
80
82
  let key = optionKey(opt.value)
81
83
  if (!map.has(key)) map.set(key, opt)
82
84
  }
83
- return Array.from(map.values())
85
+ return Array.from(map.values()).sort((a, b) => String(a.label ?? a.value ?? '').localeCompare(String(b.label ?? b.value ?? ''), undefined, {numeric: true}))
84
86
  }
85
87
 
86
88
  let multi = $derived(toBoolean(multiple))
@@ -104,7 +106,13 @@
104
106
  if (isOpen) activeIndex = ensureActiveIndex(activeIndex, filteredOptions)
105
107
  })
106
108
 
107
- function setupQuery () {
109
+ $effect(() => {
110
+ if (!sameSelection(selection, field.value)) {
111
+ setSelection(field.value, {persist: false})
112
+ }
113
+ })
114
+
115
+ function setupQuery() {
108
116
  if (!mounted) return
109
117
  let key = data ? `${data}::${value}::${resolvedLabelField || ''}` : ''
110
118
  if (key === queryKey) return
@@ -121,7 +129,7 @@
121
129
  let columns = [value]
122
130
  if (resolvedLabelField && resolvedLabelField !== value) columns.push(resolvedLabelField)
123
131
  let handler = (res: {rows?: any[]; error?: any}) => {
124
- if (res.error) return
132
+ if (!res || res.error) return
125
133
  if (!res.rows) return
126
134
  queryOptions = res.rows.map(row => ({
127
135
  value: row[value],
@@ -142,7 +150,7 @@
142
150
  closeMenu(false)
143
151
  }
144
152
 
145
- function filterOptions (opts: Option[], term: string): Option[] {
153
+ function filterOptions(opts: Option[], term: string): Option[] {
146
154
  let trimmed = term.trim().toLowerCase()
147
155
  if (!trimmed) return opts
148
156
  return opts.filter(opt => {
@@ -151,24 +159,24 @@
151
159
  })
152
160
  }
153
161
 
154
- function ensureActiveIndex (current: number, opts: Option[]): number {
162
+ function ensureActiveIndex(current: number, opts: Option[]): number {
155
163
  if (!opts.length) return -1
156
164
  if (current >= 0 && current < opts.length) return current
157
165
  let selectedIdx = opts.findIndex(opt => isOptionSelected(opt))
158
166
  return selectedIdx >= 0 ? selectedIdx : 0
159
167
  }
160
168
 
161
- function isOptionSelected (opt: Option): boolean {
169
+ function isOptionSelected(opt: Option): boolean {
162
170
  let key = optionKey(opt.value)
163
171
  return selection.some(val => optionKey(val) === key)
164
172
  }
165
173
 
166
- async function focusSearchInput () {
174
+ async function focusSearchInput() {
167
175
  await tick()
168
176
  if (searchInput) searchInput.focus()
169
177
  }
170
178
 
171
- function updateTriggerWidth () {
179
+ function updateTriggerWidth() {
172
180
  if (!triggerEl) {
173
181
  if (triggerWidth !== 0) triggerWidth = 0
174
182
  return
@@ -177,14 +185,14 @@
177
185
  if (width !== triggerWidth) triggerWidth = width
178
186
  }
179
187
 
180
- function openMenu (focusSearch = true) {
188
+ function openMenu(focusSearch = true) {
181
189
  if (isDisabled) return
182
190
  updateTriggerWidth()
183
191
  isOpen = true
184
192
  if (focusSearch) focusSearchInput()
185
193
  }
186
194
 
187
- function closeMenu (focusTrigger: boolean) {
195
+ function closeMenu(focusTrigger: boolean) {
188
196
  if (!isOpen) return
189
197
  isOpen = false
190
198
  activeIndex = -1
@@ -192,7 +200,7 @@
192
200
  if (focusTrigger) triggerEl?.focus()
193
201
  }
194
202
 
195
- function toggleMenu () {
203
+ function toggleMenu() {
196
204
  if (isDisabled) return
197
205
  if (isOpen) {
198
206
  closeMenu(false)
@@ -201,7 +209,7 @@
201
209
  }
202
210
  }
203
211
 
204
- function moveActive (delta: number) {
212
+ function moveActive(delta: number) {
205
213
  if (!filteredOptions.length) {
206
214
  activeIndex = -1
207
215
  return
@@ -215,12 +223,12 @@
215
223
  activeIndex = next
216
224
  }
217
225
 
218
- function selectActiveOption () {
226
+ function selectActiveOption() {
219
227
  if (!filteredOptions.length || activeIndex < 0 || activeIndex >= filteredOptions.length) return
220
228
  handleOptionSelect(filteredOptions[activeIndex], true)
221
229
  }
222
230
 
223
- function handleTriggerKeydown (event: KeyboardEvent) {
231
+ function handleTriggerKeydown(event: KeyboardEvent) {
224
232
  if (event.defaultPrevented) return
225
233
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp' || event.key === 'Enter' || event.key === ' ') {
226
234
  event.preventDefault()
@@ -236,7 +244,7 @@
236
244
  }
237
245
  }
238
246
 
239
- function handleMenuKeydown (event: KeyboardEvent) {
247
+ function handleMenuKeydown(event: KeyboardEvent) {
240
248
  if (event.key === 'ArrowDown') {
241
249
  event.preventDefault()
242
250
  moveActive(1)
@@ -260,30 +268,30 @@
260
268
  }
261
269
  }
262
270
 
263
- function handleOptionKeydown (event: KeyboardEvent, opt: Option) {
271
+ function handleOptionKeydown(event: KeyboardEvent, opt: Option) {
264
272
  if (event.key === 'Enter' || event.key === ' ') {
265
273
  event.preventDefault()
266
274
  handleOptionSelect(opt, true)
267
275
  }
268
276
  }
269
277
 
270
- function handleOptionSelect (opt: Option, fromKeyboard: boolean) {
278
+ function handleOptionSelect(opt: Option, fromKeyboard: boolean) {
271
279
  let key = optionKey(opt.value)
272
280
  if (multi) {
273
281
  let exists = selection.some(val => optionKey(val) === key)
274
282
  if (exists) {
275
283
  let next = selection.filter(val => optionKey(val) !== key)
276
- setSelection(next, true)
284
+ setSelection(next, {fromUser: true, persist: true})
277
285
  } else {
278
- setSelection([...selection, opt.value], true)
286
+ setSelection([...selection, opt.value], {fromUser: true, persist: true})
279
287
  }
280
288
  } else {
281
- setSelection([opt.value], true)
289
+ setSelection([opt.value], {fromUser: true, persist: true})
282
290
  closeMenu(fromKeyboard)
283
291
  }
284
292
  }
285
293
 
286
- function scrollActiveIntoView () {
294
+ function scrollActiveIntoView() {
287
295
  if (!isOpen || activeIndex < 0) return
288
296
  let optionEl = menuEl?.querySelector<HTMLElement>(`[data-index="${activeIndex}"]`)
289
297
  optionEl?.scrollIntoView({block: 'nearest'})
@@ -295,8 +303,11 @@
295
303
 
296
304
  onMount(() => {
297
305
  mounted = true
298
- let defaults = ensureArray(defaultValue)
299
- if (!hasNoDefault && defaults.length) setSelection(defaults, false)
306
+ if (field.hasExternalValue) setSelection(field.value, {persist: false})
307
+ else {
308
+ let defaults = ensureArray(defaultValue)
309
+ if (!hasNoDefault && defaults.length) setSelection(defaults, {persist: true})
310
+ }
300
311
  syncSelection(false)
301
312
  setupQuery()
302
313
  if (typeof document !== 'undefined') {
@@ -305,6 +316,7 @@
305
316
  }
306
317
  return () => {
307
318
  mounted = false
319
+ field.destroy()
308
320
  if (queryHandler) {
309
321
  window.$GRAPHENE?.unsubscribe?.(queryHandler)
310
322
  queryHandler = null
@@ -326,69 +338,77 @@
326
338
  if (triggerEl) updateTriggerWidth()
327
339
  })
328
340
 
329
- function syncSelection (fromUser: boolean) {
341
+ function syncSelection(fromUser: boolean) {
342
+ // Reconcile persisted selection with the current option set, while keeping external values
343
+ // authoritative and only applying defaults/select-all before the user has interacted.
330
344
  let opts = availableOptions
331
345
  if (!opts.length) {
332
- if (selection.length) updateInputPayload(selection)
346
+ // Keep the bound param initialized even before options load.
347
+ // This prevents $param references from throwing "Missing param" on first render.
348
+ updateInputPayload(selection)
333
349
  return
334
350
  }
335
351
  let nextSelection = selection.filter(val => valueMap.has(optionKey(val)))
336
352
  if (!fromUser) {
337
353
  let defaults = ensureArray(defaultValue)
338
- if (multi && selectAllDefault) {
354
+ if (field.hasExternalValue) {
355
+ nextSelection = nextSelection
356
+ } else if (multi && selectAllDefault) {
339
357
  nextSelection = opts.map(o => o.value)
340
358
  } else if (!touched) {
341
- if (defaults.length) {
359
+ if (defaults.length && !hasNoDefault) {
342
360
  nextSelection = defaults.filter(val => valueMap.has(optionKey(val)))
343
361
  } else if (!multi && !hasNoDefault) {
344
362
  nextSelection = selection.length ? nextSelection : []
345
363
  }
346
364
  }
347
365
  }
348
- setSelection(nextSelection, fromUser)
366
+ setSelection(nextSelection, {fromUser, persist: true})
367
+ }
368
+
369
+ function sameSelection(left: any[], right: any[]) {
370
+ let leftKeys = left.map(optionKey)
371
+ let rightKeys = right.map(optionKey)
372
+ return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index])
349
373
  }
350
374
 
351
- function setSelection (values: any[], fromUser: boolean) {
352
- let keys = values.map(optionKey)
353
- let existingKeys = selection.map(optionKey)
354
- let changed = keys.length !== existingKeys.length || keys.some((k, idx) => k !== existingKeys[idx])
355
- if (!changed) {
356
- if (!fromUser) updateInputPayload(selection)
375
+ function setSelection(values: any[], {fromUser = false, persist = true}: {fromUser?: boolean; persist?: boolean} = {}) {
376
+ if (sameSelection(values, selection)) {
377
+ if (persist && !fromUser) updateInputPayload(selection)
357
378
  return
358
379
  }
359
380
  selection = values
360
381
  if (fromUser) touched = true
361
- updateInputPayload(selection)
382
+ if (persist) updateInputPayload(selection)
362
383
  }
363
384
 
364
- function updateInputPayload (values: any[]) {
365
- let paramValue = values.length ? values[0] : null
366
- window.$GRAPHENE.updateParam(name, paramValue)
385
+ function updateInputPayload(values: any[]) {
386
+ field.set(values)
367
387
  }
368
388
 
369
- function selectAll () {
389
+ function selectAll() {
370
390
  if (!multi) return
371
- setSelection(availableOptions.map(opt => opt.value), true)
391
+ setSelection(availableOptions.map(opt => opt.value), {fromUser: true, persist: true})
372
392
  }
373
393
 
374
- function clearSelection () {
375
- setSelection([], true)
394
+ function clearSelection() {
395
+ setSelection([], {fromUser: true, persist: true})
376
396
  }
377
397
 
378
398
  let elementId = $derived(`dropdown-${name}`)
379
399
  let menuId = $derived(`${elementId}-menu`)
380
400
 
381
- function getContainerClass () {
401
+ function getContainerClass() {
382
402
  if (!hidePrint) return 'input-block'
383
403
  return 'input-block hide-print'
384
404
  }
385
405
 
386
- function getDropdownClass () {
406
+ function getDropdownClass() {
387
407
  if (!isDisabled) return 'dropdown'
388
408
  return 'dropdown is-disabled'
389
409
  }
390
410
 
391
- function getOptionClass (opt: Option, idx: number) {
411
+ function getOptionClass(opt: Option, idx: number) {
392
412
  let classes = 'dropdown-option'
393
413
  if (isOptionSelected(opt)) classes += ' is-selected'
394
414
  if (activeIndex === idx) classes += ' is-active'
@@ -496,7 +516,7 @@
496
516
  class={`checkbox-border${isOptionSelected(opt) ? ' is-filled' : ''}`}
497
517
  />
498
518
  {#if isOptionSelected(opt)}
499
- <path d="M4 8l2.5 2.5L12 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
519
+ <path d="M4 8l2.5 2.5L12 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="checkbox-checkmark" />
500
520
  {/if}
501
521
  </svg>
502
522
  {:else}
@@ -554,6 +574,7 @@
554
574
  }
555
575
  }
556
576
  .input-label {
577
+ font-family: var(--font-ui);
557
578
  font-size: 12px;
558
579
  font-weight: 600;
559
580
  color: var(--input-label-color, #374151);
@@ -583,7 +604,8 @@
583
604
  background: #ffffff;
584
605
  color: #1f2937;
585
606
  font-size: 14px;
586
- font-family: var(--ui-font-family);
607
+ font-family: var(--font-ui);
608
+ font-synthesis: none;
587
609
  cursor: pointer;
588
610
  transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
589
611
  }
@@ -658,6 +680,9 @@
658
680
  border: 1px solid #d1d5db;
659
681
  padding: 6px 10px;
660
682
  font-size: 13px;
683
+ line-height: 16px;
684
+ font-family: var(--font-ui);
685
+ font-synthesis: none;
661
686
  background: #f9fafb;
662
687
  color: inherit;
663
688
  box-sizing: border-box;
@@ -683,6 +708,9 @@
683
708
  border-radius: 8px;
684
709
  cursor: pointer;
685
710
  font-size: 14px;
711
+ line-height: 18px;
712
+ font-family: var(--font-ui);
713
+ font-synthesis: none;
686
714
  transition: background 100ms ease, color 100ms ease;
687
715
  color: #1f2937;
688
716
  }
@@ -721,6 +749,9 @@
721
749
  fill: #2563eb;
722
750
  stroke: #2563eb;
723
751
  }
752
+ .dropdown-option.is-selected .checkbox-checkmark {
753
+ stroke: #ffffff;
754
+ }
724
755
  .checkmark {
725
756
  opacity: 0;
726
757
  stroke: #2563eb;
@@ -745,6 +776,8 @@
745
776
  background: none;
746
777
  color: #2563eb;
747
778
  font-size: 13px;
779
+ font-family: var(--font-ui);
780
+ font-synthesis: none;
748
781
  cursor: pointer;
749
782
  padding: 4px 0;
750
783
  }
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import {getContext, onMount} from 'svelte'
3
- import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
4
3
 
5
4
  interface Props {
6
5
  value: any
@@ -10,7 +9,7 @@
10
9
  let {value, valueLabel = undefined}: Props = $props()
11
10
 
12
11
  type RegisterFn = ((option: {value: any; label: string}) => (() => void) | void) | undefined
13
- const register = getContext<RegisterFn>(DROPDOWN_CONTEXT)
12
+ const register = getContext<RegisterFn>('dropdown')
14
13
 
15
14
  let unregister: (() => void) | void
16
15