@graphenedata/cli 0.0.15 → 0.0.17

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 (117) hide show
  1. package/README.md +174 -0
  2. package/dist/cli/bigQuery-OQUNH3VT.js +75 -0
  3. package/dist/cli/bigQuery-OQUNH3VT.js.map +7 -0
  4. package/dist/cli/chunk-56K2FF57.js +53 -0
  5. package/dist/cli/chunk-56K2FF57.js.map +7 -0
  6. package/dist/cli/chunk-TZTTALAV.js +12868 -0
  7. package/dist/cli/chunk-TZTTALAV.js.map +7 -0
  8. package/dist/cli/cli.js +260 -11196
  9. package/dist/cli/clickhouse-S3BJSKND.js +65 -0
  10. package/dist/cli/clickhouse-S3BJSKND.js.map +7 -0
  11. package/dist/cli/duckdb-TKVMONRK.js +87 -0
  12. package/dist/cli/duckdb-TKVMONRK.js.map +7 -0
  13. package/dist/cli/serve2-S2LL4D4D.js +448 -0
  14. package/dist/cli/serve2-S2LL4D4D.js.map +7 -0
  15. package/dist/cli/snowflake-3VPDEYYP.js +128 -0
  16. package/dist/cli/snowflake-3VPDEYYP.js.map +7 -0
  17. package/dist/index.d.ts +63 -0
  18. package/dist/lang/index.d.ts +63 -0
  19. package/dist/skills/graphene/SKILL.md +156 -95
  20. package/dist/skills/graphene/references/big-value.md +6 -41
  21. package/dist/skills/graphene/references/date-range.md +64 -0
  22. package/dist/skills/graphene/references/dropdown.md +3 -4
  23. package/dist/skills/graphene/references/echarts.md +162 -0
  24. package/dist/skills/graphene/references/gsql.md +55 -25
  25. package/dist/skills/graphene/references/model-gsql.md +70 -0
  26. package/dist/skills/graphene/references/table.md +13 -14
  27. package/dist/skills/graphene/references/text-input.md +2 -1
  28. package/dist/ui/app.css +239 -340
  29. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  30. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  31. package/dist/ui/component-utilities/enrich.ts +793 -0
  32. package/dist/ui/component-utilities/format.ts +177 -0
  33. package/dist/ui/component-utilities/inputUtils.ts +44 -8
  34. package/dist/ui/component-utilities/theme.ts +200 -0
  35. package/dist/ui/component-utilities/themeStores.ts +21 -8
  36. package/dist/ui/component-utilities/types.ts +70 -0
  37. package/dist/ui/components/AreaChart.svelte +57 -105
  38. package/dist/ui/components/BarChart.svelte +71 -129
  39. package/dist/ui/components/BigValue.svelte +24 -40
  40. package/dist/ui/components/Column.svelte +10 -18
  41. package/dist/ui/components/DateRange.svelte +54 -21
  42. package/dist/ui/components/Dropdown.svelte +47 -26
  43. package/dist/ui/components/DropdownOption.svelte +1 -2
  44. package/dist/ui/components/ECharts.svelte +181 -67
  45. package/dist/ui/components/InlineDelta.svelte +50 -31
  46. package/dist/ui/components/LineChart.svelte +54 -125
  47. package/dist/ui/components/PieChart.svelte +27 -37
  48. package/dist/ui/components/QueryLoad.svelte +77 -45
  49. package/dist/ui/components/Row.svelte +2 -1
  50. package/dist/ui/components/ScatterPlot.svelte +52 -0
  51. package/dist/ui/components/Skeleton.svelte +32 -0
  52. package/dist/ui/components/Table.svelte +3 -2
  53. package/dist/ui/components/TableGroupRow.svelte +28 -36
  54. package/dist/ui/components/TableHarness.svelte +32 -0
  55. package/dist/ui/components/TableHeader.svelte +34 -59
  56. package/dist/ui/components/TableRow.svelte +14 -38
  57. package/dist/ui/components/TableSubtotalRow.svelte +18 -21
  58. package/dist/ui/components/TableTotalRow.svelte +27 -37
  59. package/dist/ui/components/TextInput.svelte +13 -12
  60. package/dist/ui/components/Value.svelte +25 -0
  61. package/dist/ui/components/_Table.svelte +72 -70
  62. package/dist/ui/internal/ChartGallery.svelte +527 -0
  63. package/dist/ui/internal/ErrorDisplay.svelte +22 -97
  64. package/dist/ui/internal/LocalApp.svelte +84 -19
  65. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  66. package/dist/ui/internal/Sidebar.svelte +178 -0
  67. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  68. package/dist/ui/internal/StyleGallery.svelte +244 -0
  69. package/dist/ui/internal/clientCache.ts +2 -2
  70. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  71. package/dist/ui/internal/queryEngine.ts +112 -129
  72. package/dist/ui/internal/runSocket.ts +31 -14
  73. package/dist/ui/internal/sidebar.svelte.js +18 -0
  74. package/dist/ui/internal/telemetry.ts +51 -16
  75. package/dist/ui/internal/types.d.ts +7 -0
  76. package/dist/ui/web.js +30 -11
  77. package/package.json +40 -38
  78. package/dist/skills/graphene/references/area-chart.md +0 -95
  79. package/dist/skills/graphene/references/bar-chart.md +0 -112
  80. package/dist/skills/graphene/references/line-chart.md +0 -108
  81. package/dist/skills/graphene/references/pie-chart.md +0 -29
  82. package/dist/skills/graphene/references/value-formats.md +0 -104
  83. package/dist/ui/component-utilities/autoFormatting.js +0 -280
  84. package/dist/ui/component-utilities/builtInFormats.js +0 -481
  85. package/dist/ui/component-utilities/chartContext.js +0 -12
  86. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  87. package/dist/ui/component-utilities/checkInputs.js +0 -84
  88. package/dist/ui/component-utilities/convert.js +0 -15
  89. package/dist/ui/component-utilities/dateParsing.js +0 -56
  90. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  91. package/dist/ui/component-utilities/echarts.js +0 -252
  92. package/dist/ui/component-utilities/echartsThemes.js +0 -443
  93. package/dist/ui/component-utilities/formatTitle.js +0 -24
  94. package/dist/ui/component-utilities/formatting.js +0 -241
  95. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  96. package/dist/ui/component-utilities/getColumnSummary.js +0 -62
  97. package/dist/ui/component-utilities/getCompletedData.js +0 -122
  98. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  99. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  100. package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
  101. package/dist/ui/component-utilities/getSortedData.js +0 -9
  102. package/dist/ui/component-utilities/getStackPercentages.js +0 -45
  103. package/dist/ui/component-utilities/getStackedData.js +0 -19
  104. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  105. package/dist/ui/component-utilities/globalContexts.js +0 -1
  106. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  107. package/dist/ui/component-utilities/replaceNulls.js +0 -16
  108. package/dist/ui/component-utilities/tableUtils.ts +0 -107
  109. package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
  110. package/dist/ui/components/Area.svelte +0 -214
  111. package/dist/ui/components/Bar.svelte +0 -347
  112. package/dist/ui/components/Chart.svelte +0 -995
  113. package/dist/ui/components/Line.svelte +0 -227
  114. package/dist/ui/internal/NavSidebar.svelte +0 -396
  115. package/dist/ui/internal/theme.ts +0 -60
  116. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  117. package/dist/ui/public/inter-latin.woff2 +0 -0
@@ -1,45 +1,110 @@
1
- <script>
2
- import {errorProvider} from './telemetry.ts'
1
+ <script lang="ts">
2
+ import {onDestroy, onMount, tick} from 'svelte'
3
+ import {setErrorFor} from './telemetry.ts'
4
+ import {PageInputs, activatePageInputs, releasePageInputs, setPageInputsContext} from './pageInputs.svelte.js'
3
5
  import navFiles from 'virtual:nav'
4
- import NavSidebar from './NavSidebar.svelte'
6
+ import Sidebar from './Sidebar.svelte'
7
+ import SidebarToggle from './SidebarToggle.svelte'
8
+ import PageNavGroup from './PageNavGroup.svelte'
5
9
  import ErrorDisplay from './ErrorDisplay.svelte'
10
+ import ChartGallery from './ChartGallery.svelte'
11
+ import StyleGallery from './StyleGallery.svelte'
12
+ import {type GrapheneError} from '../../lang/index.js'
13
+
14
+ let pageInputs = activatePageInputs(new PageInputs())
15
+ setPageInputsContext(pageInputs)
16
+ onDestroy(() => releasePageInputs(pageInputs))
6
17
 
7
18
  // Nav sidebar with HMR support for the virtual file list
8
19
  let navData = $state(navFiles)
9
20
  import.meta.hot?.accept('virtual:nav', mod => navData = mod.default)
10
21
 
22
+ let pathName = window.location.pathname.replace(/^\//, '') || 'index'
23
+
11
24
  // Track compile errors from both initial load and subsequent HMR failures.
12
- // Uses errorProvider so `check` can report compilation errors.
13
- let compileError = $state(null)
14
- errorProvider('compile', () => compileError ? [compileError] : [])
25
+ let compileError = $state<GrapheneError | null>(null)
15
26
  import.meta.hot?.on('vite:error', (payload) => {
16
- compileError = payload.err
17
- compileError.type = 'compile'
18
- compileError.file = payload.err.id
27
+ let path = String(payload.err.id || '').split('?')[0].replace(/^file:\/\//, '').replace(/\\/g, '/').replace(/^\/+/, '')
28
+ if (!path.endsWith(pathName + '.md')) return // ignore errors on md pages that are not this page
29
+
30
+ let line = Math.max(0, (payload.err.loc?.line || 1) - 1)
31
+ let col = Math.max(0, payload.err.loc?.column || 0)
32
+ compileError = {
33
+ message: String(payload.err.message || '').replace(/^.*?:\d+:\d+\s*/, '').replace(/\s*https:\/\/svelte\.dev\/\S+/g, '').trim(),
34
+ frame: payload.err.frame,
35
+ file: path,
36
+ from: {line, col, offset: 0},
37
+ to: {line, col: col + 1, offset: 0},
38
+ }
39
+ setErrorFor('compile', compileError)
19
40
  Page = null
20
41
  })
21
42
 
22
43
  // The md file is dynamically imported, so even if there's a compile error, we'll still load LocalApp and can show the user the issue
23
- let Page = $state(null)
24
- let pathName = window.location.pathname.replace(/^\//, '') || 'index'
25
- if (pathName !== '__ct') {
26
- import(/* @vite-ignore */ '/' + pathName + '.md').then(mod => {
27
- Page = mod.default
28
- compileError = null
29
- }).catch(() => {})
30
- }
44
+ let Page = $state<any>(null)
45
+ let pageMeta = $state<any>({})
46
+
47
+ onMount(async () => {
48
+ try {
49
+ // force fonts to load before we mount the component.
50
+ // This is important for echarts, as it measures text and if done with the wrong font, then
51
+ // a) when the right font loads, things will just slightly not line up with edges
52
+ // b) test snapshots will differ, as they measure with whatever the system sans font is
53
+ // c) screenshots taken by `graphene run` might have the wrong font
54
+ document.fonts.load("12px 'Source Sans 3'")
55
+ await document.fonts.ready
56
+
57
+ if (pathName == '_charts') {
58
+ Page = ChartGallery
59
+ } else if (pathName == '_styles') {
60
+ Page = StyleGallery
61
+ } else if (pathName !== '__ct') {
62
+ let mod = await import(/* @vite-ignore */ '/' + pathName + '.md')
63
+ Page = mod.default
64
+ pageMeta = mod.metadata || {}
65
+ compileError = null
66
+ setErrorFor('compile', null)
67
+ }
68
+ } finally {
69
+ await tick()
70
+ window.$GRAPHENE.appLoading = false
71
+ }
72
+ })
31
73
  </script>
32
74
 
33
- <nav id="nav"><NavSidebar files={navData} /></nav>
34
- <main id="content">
75
+ <SidebarToggle style='position:fixed;top:2rem;left:2rem;opacity:0.3;' />
76
+ <Sidebar>
77
+ <PageNavGroup files={navData} />
78
+ </Sidebar>
79
+
80
+ <main id="content" class={{pageContent: compileError || !!Page, dashboardLayout: pageMeta.layout == 'dashboard'}}>
35
81
  {#if compileError}
36
82
  <h1 class="page-error-heading">Error loading page</h1>
37
83
  <ErrorDisplay error={compileError} />
38
84
  {:else if Page}
85
+ {#if pageMeta.title}
86
+ <h1>{pageMeta.title}</h1>
87
+ {/if}
39
88
  <Page />
40
89
  {/if}
41
90
  </main>
42
91
 
43
92
  <style>
93
+ main.pageContent {
94
+ margin: 0 auto;
95
+ min-width: 0;
96
+ padding: 20px 6rem 80px;
97
+ max-width: 720px;
98
+ }
99
+
100
+ main.pageContent.dashboardLayout {
101
+ max-width: 1200px;
102
+ }
103
+
44
104
  .page-error-heading { margin-top: 0; }
105
+
106
+ /* want to control this margin so it lines up with the SidebarToggle */
107
+ main h1:first-child {
108
+ margin-top: 12px;
109
+ }
45
110
  </style>
@@ -0,0 +1,269 @@
1
+ <script>
2
+ import {SvelteSet, SvelteMap} from 'svelte/reactivity'
3
+
4
+ let {currentFile = '', files = [], onNavigate = undefined, baseRoute = ''} = $props()
5
+
6
+ let tree = $state([])
7
+ // eslint-disable-next-line svelte/no-unnecessary-state-wrap -- reassigned, needs $state
8
+ let openFolders = $state(new SvelteSet())
9
+ let treeSignature = $state('')
10
+ let lastCurrent = $state('')
11
+
12
+ let navFiles = $derived((files || []).map(normalizeNavFile))
13
+ let normalizedFiles = $derived(navFiles.map(f => f.path))
14
+ let titlesByPath = $derived(Object.fromEntries(navFiles.filter(f => !!f.title).map(f => [f.path, f.title])))
15
+
16
+ let normalizedCurrent = $derived(deriveCurrentFile(currentFile, normalizedFiles))
17
+ let currentRoute = $derived(normalizedCurrent ? pathToRoute(normalizedCurrent) : '/')
18
+
19
+ function deriveCurrentFile(_currentFile, _normalizedFiles) {
20
+ let fromProp = normalizeFilePath(_currentFile)
21
+ let route = getLocationRoute()
22
+ if (route && _normalizedFiles) {
23
+ let match = _normalizedFiles.find(f => pathToRoute(f) === route)
24
+ if (match) return match
25
+ }
26
+ return fromProp
27
+ }
28
+
29
+ function normalizeNavFile(file) {
30
+ if (!file || typeof file.path !== 'string') throw new Error('PageNavGroup files must be {path, title?} objects')
31
+ return {path: normalizeFilePath(file.path), title: file.title || undefined}
32
+ }
33
+
34
+ function normalizeFilePath(filePath) {
35
+ return (filePath || '').replace(/^\.\//, '').replace(/\\/g, '/').replace(/^\/+/, '')
36
+ }
37
+
38
+ function getLocationRoute() {
39
+ if (typeof window === 'undefined') return null
40
+ return (window.location.pathname || '/').replace(/\/+$/, '') || '/'
41
+ }
42
+
43
+ $effect(() => {
44
+ let nextSignature = navFiles.map(f => `${f.path}:${f.title || ''}`).join('|')
45
+ if (nextSignature !== treeSignature) {
46
+ treeSignature = nextSignature
47
+ tree = buildTree(normalizedFiles, titlesByPath)
48
+ openFolders = mergeAncestorFolders(new SvelteSet(), normalizedCurrent)
49
+ }
50
+ })
51
+
52
+ $effect(() => {
53
+ if (normalizedCurrent !== lastCurrent) {
54
+ openFolders = mergeAncestorFolders(openFolders, normalizedCurrent)
55
+ lastCurrent = normalizedCurrent
56
+ }
57
+ })
58
+
59
+ function toggleFolder(path) {
60
+ if (!path) return
61
+ let next = new SvelteSet(openFolders)
62
+ if (next.has(path)) next.delete(path)
63
+ else next.add(path)
64
+ openFolders = next
65
+ }
66
+
67
+ function buildTree(paths, titleLookup = {}) {
68
+ let root = []
69
+ let folderMap = new SvelteMap()
70
+
71
+ for (let filePath of paths) {
72
+ let segments = filePath.split('/')
73
+ if (!segments.length) continue
74
+ let fileName = segments.pop()
75
+ let parentChildren = root
76
+ let parentPath = ''
77
+
78
+ for (let segment of segments) {
79
+ parentPath = parentPath ? `${parentPath}/${segment}` : segment
80
+ if (!folderMap.has(parentPath)) {
81
+ let folderNode = {type: 'folder', name: segment, label: formatLabel(segment, 'folder'), path: parentPath, children: [], route: null}
82
+ folderMap.set(parentPath, folderNode)
83
+ parentChildren.push(folderNode)
84
+ }
85
+ parentChildren = folderMap.get(parentPath).children
86
+ }
87
+
88
+ if (!fileName) continue
89
+ let fullPath = parentPath ? `${parentPath}/${fileName}` : fileName
90
+
91
+ // An index.md becomes the folder's own route rather than a separate leaf.
92
+ if (fileName.toLowerCase() === 'index.md' && parentPath) {
93
+ let folderNode = folderMap.get(parentPath)
94
+ if (folderNode) {
95
+ folderNode.route = pathToRoute(fullPath)
96
+ if (titleLookup[fullPath]) folderNode.label = titleLookup[fullPath]
97
+ }
98
+ continue
99
+ }
100
+
101
+ if (parentChildren.find(n => n.path === fullPath)) continue
102
+ parentChildren.push({
103
+ type: 'file',
104
+ name: fileName,
105
+ label: formatLabel(fileName, 'file', titleLookup[fullPath]),
106
+ path: fullPath,
107
+ route: pathToRoute(fullPath),
108
+ })
109
+ }
110
+
111
+ return sortNodes(root)
112
+ }
113
+
114
+ function sortNodes(nodes) {
115
+ return nodes
116
+ .map(n => n.type === 'folder' && n.children?.length ? {...n, children: sortNodes(n.children)} : n)
117
+ .sort((a, b) => {
118
+ if (a.label === 'Home') return -1
119
+ if (b.label === 'Home') return 1
120
+ if (a.type !== b.type) return a.type === 'folder' ? -1 : 1
121
+ return a.label.localeCompare(b.label)
122
+ })
123
+ }
124
+
125
+ function mergeAncestorFolders(openSet, filePath) {
126
+ let next = new SvelteSet(openSet)
127
+ if (!filePath) return next
128
+ let parts = filePath.split('/')
129
+ parts.pop()
130
+ let aggregate = []
131
+ for (let part of parts) {
132
+ aggregate.push(part)
133
+ next.add(aggregate.join('/'))
134
+ }
135
+ return next
136
+ }
137
+
138
+ function formatLabel(value, type, explicitTitle = undefined) {
139
+ if (explicitTitle) return explicitTitle
140
+ let cleaned = type === 'file' ? value.replace(/\.md$/, '') : value
141
+ if (cleaned.toLowerCase() === 'index') return 'Home'
142
+ return cleaned.split(/[\s_-]+/).filter(Boolean)
143
+ .map(c => c.charAt(0).toUpperCase() + c.slice(1)).join(' ')
144
+ }
145
+
146
+ function pathToRoute(path) {
147
+ let clean = path.replace(/\.md$/, '')
148
+ let prefix = baseRoute ? '/' + baseRoute : ''
149
+ if (!clean || clean === 'index') return prefix || '/'
150
+ return prefix + '/' + clean
151
+ }
152
+
153
+ function handleLinkClick(event, href) {
154
+ if (!onNavigate) return
155
+ if (href.startsWith('http') || href.startsWith('//')) return
156
+ event.preventDefault()
157
+ onNavigate(href)
158
+ }
159
+ </script>
160
+
161
+ <div class="sb-group">
162
+ <ul class="sb-menu">
163
+ {#each tree as node (node.path)}
164
+ {@render Row(node)}
165
+ {/each}
166
+ </ul>
167
+ </div>
168
+
169
+ {#snippet Chevron(open)}
170
+ <svg
171
+ class={open ? 'sb-chevron open' : 'sb-chevron'}
172
+ viewBox="0 0 24 24" fill="none" stroke="currentColor"
173
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
174
+ aria-hidden="true"
175
+ ><path d="m9 18 6-6-6-6"/></svg>
176
+ {/snippet}
177
+
178
+ {#snippet ChevronSpacer()}
179
+ <svg class="sb-chevron" viewBox="0 0 24 24" aria-hidden="true"></svg>
180
+ {/snippet}
181
+
182
+ {#snippet ChevronToggle(node, open)}
183
+ <!-- role='button' inside the anchor so we don't nest <button> in <a> (invalid HTML). -->
184
+ <span
185
+ class="chev-toggle"
186
+ role="button"
187
+ tabindex="0"
188
+ data-folder-toggle={node.path}
189
+ aria-expanded={open}
190
+ aria-label={(open ? 'Collapse ' : 'Expand ') + node.label}
191
+ onclick={(e) => { e.preventDefault(); e.stopPropagation(); toggleFolder(node.path) }}
192
+ onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); toggleFolder(node.path) } }}
193
+ >
194
+ {@render Chevron(open)}
195
+ </span>
196
+ {/snippet}
197
+
198
+ {#snippet Row(node)}
199
+ <li data-folder={node.type === 'folder' ? node.path : undefined}>
200
+ {#if node.type === 'folder'}
201
+ {@const open = openFolders.has(node.path)}
202
+ {#if node.route}
203
+ <!-- Folder that also has an index.md: chevron toggles, label navigates. -->
204
+ <a
205
+ class={node.route === currentRoute ? 'sb-item active' : 'sb-item'}
206
+ href={node.route}
207
+ title={node.label}
208
+ aria-current={node.route === currentRoute ? 'page' : undefined}
209
+ onclick={(e) => handleLinkClick(e, node.route)}
210
+ >
211
+ {@render ChevronToggle(node, open)}
212
+ <span class="sb-label">{node.label}</span>
213
+ </a>
214
+ {:else}
215
+ <button
216
+ class="sb-item"
217
+ type="button"
218
+ title={node.label}
219
+ data-folder-toggle={node.path}
220
+ aria-expanded={open}
221
+ onclick={() => toggleFolder(node.path)}
222
+ >
223
+ {@render Chevron(open)}
224
+ <span class="sb-label">{node.label}</span>
225
+ </button>
226
+ {/if}
227
+ {#if open && node.children?.length}
228
+ <ul class="sb-sub">
229
+ {#each node.children as child (child.path)}
230
+ {@render Row(child)}
231
+ {/each}
232
+ </ul>
233
+ {/if}
234
+ {:else}
235
+ <a
236
+ class={node.path === normalizedCurrent ? 'sb-item active' : 'sb-item'}
237
+ href={node.route}
238
+ title={node.label}
239
+ aria-current={node.path === normalizedCurrent ? 'page' : undefined}
240
+ onclick={(e) => handleLinkClick(e, node.route)}
241
+ >
242
+ <!-- Invisible chevron spacer so file labels align with folder labels. -->
243
+ {@render ChevronSpacer()}
244
+ <span class="sb-label">{node.label}</span>
245
+ </a>
246
+ {/if}
247
+ </li>
248
+ {/snippet}
249
+
250
+ <style>
251
+ /* Clickable chevron inside a folder-with-route link. Stops propagation so
252
+ clicking here toggles the folder instead of navigating. */
253
+ .chev-toggle {
254
+ display: inline-flex;
255
+ align-items: center;
256
+ justify-content: center;
257
+ margin: -0.25rem -0.25rem -0.25rem -0.25rem;
258
+ padding: 0.25rem;
259
+ border-radius: 0.25rem;
260
+ cursor: pointer;
261
+ flex-shrink: 0;
262
+ }
263
+ .chev-toggle:hover :global(.sb-chevron) { color: var(--sidebar-foreground, #252525); }
264
+ .chev-toggle:focus-visible {
265
+ outline: 2px solid var(--sidebar-ring, #b5b5b5);
266
+ outline-offset: 1px;
267
+ }
268
+
269
+ </style>
@@ -0,0 +1,178 @@
1
+ <script>
2
+ // Content-agnostic sidebar shell. The panel slides in as an overlay when the
3
+ // user hovers the paired SidebarToggle button (rendered separately by the host)
4
+ // or the panel itself. Styling values are ported from shadcn-svelte's sidebar registry.
5
+ import {sidebar} from './sidebar.svelte.js'
6
+ let {children, width = '16rem'} = $props()
7
+ </script>
8
+
9
+ <nav
10
+ id="nav"
11
+ class="sb-panel pretty-scrollbar"
12
+ style="--sb-w:{width}"
13
+ data-open={sidebar.open}
14
+ onmouseenter={sidebar.enter}
15
+ onmouseleave={sidebar.leave}
16
+ >
17
+ <div class="sb-inner">
18
+ {@render children?.()}
19
+ </div>
20
+ </nav>
21
+
22
+ <style>
23
+ .sb-panel {
24
+ position: fixed;
25
+ top: 0;
26
+ left: 0;
27
+ z-index: 40;
28
+ height: 100vh;
29
+ width: var(--sb-w);
30
+ background: var(--sidebar);
31
+ color: var(--sidebar-foreground);
32
+ border-right: 1px solid var(--sidebar-border);
33
+ transform: translateX(-102%);
34
+ transition: transform 200ms ease;
35
+ overflow-y: auto;
36
+ overflow-x: hidden;
37
+ /* Prevent rubber-band over-scroll from revealing content behind the panel. */
38
+ overscroll-behavior: none;
39
+ pointer-events: none;
40
+ }
41
+ .sb-panel[data-open='true'] {
42
+ transform: translateX(0);
43
+ pointer-events: auto;
44
+ }
45
+
46
+ .sb-inner {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 0;
50
+ margin-top: 24px;
51
+ font-family: var(--ui-font-family);
52
+ }
53
+
54
+ /* ============================================================
55
+ Styling primitives scoped to the panel so content can use
56
+ them as plain CSS classes. Ported from shadcn-svelte.
57
+ ============================================================ */
58
+
59
+ /* Group: `p-2`. Items fill the width inside this padding, with `rounded-md`,
60
+ matching shadcn exactly. */
61
+ .sb-panel :global(.sb-group) {
62
+ display: flex;
63
+ flex-direction: column;
64
+ padding: 0.5rem;
65
+ min-width: 0;
66
+ }
67
+
68
+ /* Group label: `h-8 px-2 text-xs font-medium` at 70% foreground */
69
+ .sb-panel :global(.sb-group-label) {
70
+ display: flex;
71
+ align-items: center;
72
+ height: 2rem;
73
+ padding: 0 0.5rem;
74
+ font-size: 0.75rem;
75
+ font-weight: 500;
76
+ color: var(--sidebar-foreground);
77
+ opacity: 0.7;
78
+ border-radius: 0.375rem;
79
+ }
80
+
81
+ /* Menu (ul): `flex flex-col gap-1 w-full min-w-0`.
82
+ Reset app.css's global <ul> list styling (list-disc, padding-inline-start). */
83
+ .sb-panel :global(.sb-menu),
84
+ .sb-panel :global(.sb-sub) {
85
+ list-style: none;
86
+ margin: 0;
87
+ padding: 0;
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 0.25rem;
91
+ min-width: 0;
92
+ }
93
+ /* Only the top-level menu fills its group; sub-menus let their width be
94
+ derived from `margin` + parent width, otherwise `width: 100%` + margin
95
+ overflows the parent and breaks child ellipsis clipping. */
96
+ .sb-panel :global(.sb-menu) { width: 100%; }
97
+ .sb-panel :global(.sb-menu li),
98
+ .sb-panel :global(.sb-sub li) {
99
+ list-style: none;
100
+ margin: 0;
101
+ width: 100%;
102
+ min-width: 0;
103
+ }
104
+ .sb-panel :global(.sb-menu li + li),
105
+ .sb-panel :global(.sb-sub li + li) { margin-top: 0; }
106
+
107
+ /* Sub menu: indented under its parent. Shadcn uses a `border-s` guide line
108
+ here, but that only reads well when items have leading icons. Without
109
+ icons, the line floats in empty space, so we drop it (and the paired
110
+ translate-x-px trick that existed to cover it). */
111
+ .sb-panel :global(.sb-sub) {
112
+ margin: 0 0.875rem;
113
+ padding: 0.125rem 0.625rem;
114
+ gap: 0.25rem;
115
+ }
116
+
117
+ /* Menu item: `flex h-8 w-full items-center gap-2 rounded-md p-2 text-sm`,
118
+ hover/active → `bg-sidebar-accent text-sidebar-accent-foreground`,
119
+ active → `font-medium`, focus-visible → `ring-2`. */
120
+ .sb-panel :global(.sb-item) {
121
+ display: flex;
122
+ width: 100%;
123
+ align-items: center;
124
+ gap: 0.2rem;
125
+ height: 2rem;
126
+ padding: 0 0.5rem;
127
+ border-radius: 0.375rem;
128
+ font-size: 0.875rem;
129
+ line-height: 1.25rem;
130
+ color: var(--sidebar-foreground);
131
+ text-decoration: none;
132
+ background: transparent;
133
+ border: none;
134
+ cursor: pointer;
135
+ text-align: start;
136
+ white-space: nowrap;
137
+ overflow: hidden;
138
+ box-sizing: border-box;
139
+ }
140
+ .sb-panel :global(.sb-item:hover) {
141
+ background: var(--sidebar-accent);
142
+ color: var(--sidebar-accent-foreground);
143
+ text-decoration: none;
144
+ }
145
+ .sb-panel :global(.sb-item.active) {
146
+ background: var(--sidebar-accent);
147
+ color: var(--sidebar-accent-foreground);
148
+ font-weight: 500;
149
+ }
150
+ .sb-panel :global(.sb-item:focus-visible) {
151
+ outline: 2px solid var(--sidebar-ring);
152
+ outline-offset: -2px;
153
+ }
154
+
155
+ /* Sub-button variant: slightly shorter (h-7 vs h-8), per shadcn. */
156
+ .sb-panel :global(.sb-sub .sb-item) {
157
+ height: 1.75rem;
158
+ }
159
+
160
+ .sb-panel :global(.sb-item .sb-label) {
161
+ flex: 1;
162
+ min-width: 0; /* allow the flex child to shrink below its intrinsic size */
163
+ white-space: nowrap;
164
+ overflow: hidden;
165
+ text-overflow: ellipsis;
166
+ }
167
+
168
+ /* Chevron: 1rem Lucide SVG with 200ms rotate transition. */
169
+ .sb-panel :global(.sb-chevron) {
170
+ flex-shrink: 0;
171
+ width: 1rem;
172
+ height: 1rem;
173
+ color: var(--sidebar-foreground);
174
+ opacity: 0.6;
175
+ transition: transform 200ms ease;
176
+ }
177
+ .sb-panel :global(.sb-chevron.open) { transform: rotate(90deg); }
178
+ </style>
@@ -0,0 +1,47 @@
1
+ <script>
2
+ // Hamburger button that opens the paired <Sidebar>. Positioning is up to the
3
+ // host (typically fixed in a corner); hover state is shared via context so
4
+ // moving between the button and the panel keeps the sidebar open.
5
+ import {sidebar} from './sidebar.svelte.js'
6
+ let {style} = $props()
7
+ </script>
8
+
9
+ <button
10
+ class="sb-trigger"
11
+ type="button"
12
+ aria-label="Toggle navigation"
13
+ aria-expanded={sidebar.open}
14
+ onmouseenter={sidebar.enter}
15
+ onmouseleave={sidebar.leave}
16
+ onfocus={sidebar.enter}
17
+ onblur={sidebar.leave}
18
+ style={style}
19
+ >
20
+ <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
21
+ <path d="M4 6h16M4 12h16M4 18h16"/>
22
+ </svg>
23
+ </button>
24
+
25
+ <style>
26
+ .sb-trigger {
27
+ width: 2rem;
28
+ height: 2rem;
29
+ display: inline-flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ color: var(--sidebar-foreground);
33
+ background: transparent;
34
+ border: none;
35
+ border-radius: 0.375rem;
36
+ cursor: pointer;
37
+ opacity: 0.7;
38
+ transition: opacity 120ms ease, background-color 120ms ease;
39
+ z-index: 39;
40
+ }
41
+ .sb-trigger:hover { opacity: 1; background: var(--sidebar-accent); }
42
+ .sb-trigger:focus-visible {
43
+ opacity: 1;
44
+ outline: 2px solid var(--sidebar-ring);
45
+ outline-offset: 2px;
46
+ }
47
+ </style>