@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,40 +1,108 @@
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'
5
- import PageError from './PageError.svelte'
6
+ import Sidebar from './Sidebar.svelte'
7
+ import SidebarToggle from './SidebarToggle.svelte'
8
+ import PageNavGroup from './PageNavGroup.svelte'
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
 
11
22
  // 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] : [])
23
+ let compileError = $state<GrapheneError | null>(null)
15
24
  import.meta.hot?.on('vite:error', (payload) => {
16
- compileError = payload.err
17
- compileError.type = 'compile'
18
- compileError.file = payload.err.id
25
+ let line = Math.max(0, (payload.err.loc?.line || 1) - 1)
26
+ let col = Math.max(0, payload.err.loc?.column || 0)
27
+ let path = String(payload.err.id || '').replace(/^file:\/\//, '').replace(/\\/g, '/').replace(/^\/+/, '')
28
+ let message = String(payload.err.message || '').replace(/^.*?:\d+:\d+\s*/, '').replace(/\s*https:\/\/svelte\.dev\/\S+/g, '').trim()
29
+ compileError = {
30
+ message,
31
+ frame: payload.err.frame,
32
+ file: path,
33
+ from: {line, col, offset: 0},
34
+ to: {line, col: col + 1, offset: 0},
35
+ }
36
+ setErrorFor('compile', compileError)
19
37
  Page = null
20
38
  })
21
39
 
22
40
  // 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 => {
41
+ let Page = $state<any>(null)
42
+ let pageMeta = $state<any>({})
43
+ let pageReadyResolve: (() => void) | undefined
44
+ window.$GRAPHENE.pageReady = new Promise<void>(resolve => pageReadyResolve = resolve)
45
+
46
+ onMount(async () => {
47
+ let pathName = window.location.pathname.replace(/^\//, '') || 'index'
48
+
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')
27
63
  Page = mod.default
64
+ pageMeta = mod.metadata || {}
28
65
  compileError = null
29
- }).catch(() => {})
30
- }
66
+ setErrorFor('compile', null)
67
+ }
68
+ await tick()
69
+ pageReadyResolve?.()
70
+ })
31
71
  </script>
32
72
 
33
- <nav id="nav"><NavSidebar files={navData} /></nav>
34
- <main id="content">
73
+ <SidebarToggle style='position:fixed;top:2rem;left:2rem;opacity:0.3;' />
74
+ <Sidebar>
75
+ <PageNavGroup files={navData} />
76
+ </Sidebar>
77
+
78
+ <main id="content" class={{pageContent: !!Page, dashboardLayout: pageMeta.layout == 'dashboard'}}>
35
79
  {#if compileError}
36
- <PageError error={compileError} />
80
+ <h1 class="page-error-heading">Error loading page</h1>
81
+ <ErrorDisplay error={compileError} />
37
82
  {:else if Page}
83
+ {#if pageMeta.title}
84
+ <h1>{pageMeta.title}</h1>
85
+ {/if}
38
86
  <Page />
39
87
  {/if}
40
88
  </main>
89
+
90
+ <style>
91
+ main.pageContent {
92
+ margin: 0 auto;
93
+ min-width: 0;
94
+ padding: 20px 6rem 80px;
95
+ max-width: 720px;
96
+ }
97
+
98
+ main.pageContent.dashboardLayout {
99
+ max-width: 1200px;
100
+ }
101
+
102
+ .page-error-heading { margin-top: 0; }
103
+
104
+ /* want to control this margin so it lines up with the SidebarToggle */
105
+ main h1:first-child {
106
+ margin-top: 12px;
107
+ }
108
+ </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>