@graphenedata/cli 0.0.15 → 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.
- package/README.md +138 -0
- package/dist/cli/bigQuery-I3F46SC6.js +75 -0
- package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
- package/dist/cli/chunk-OVWODUTJ.js +12849 -0
- package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
- package/dist/cli/chunk-QAXEOZ43.js +53 -0
- package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
- package/dist/cli/cli.js +234 -11197
- package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
- package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
- package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
- package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
- package/dist/cli/serve2-TNN5EROW.js +447 -0
- package/dist/cli/serve2-TNN5EROW.js.map +7 -0
- package/dist/cli/snowflake-MOQB5GA4.js +128 -0
- package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
- package/dist/index.d.ts +63 -0
- package/dist/lang/index.d.ts +63 -0
- package/dist/skills/graphene/SKILL.md +150 -96
- package/dist/skills/graphene/references/big-value.md +6 -41
- package/dist/skills/graphene/references/date-range.md +64 -0
- package/dist/skills/graphene/references/dropdown.md +3 -4
- package/dist/skills/graphene/references/echarts.md +162 -0
- package/dist/skills/graphene/references/gsql.md +55 -25
- package/dist/skills/graphene/references/model-gsql.md +72 -0
- package/dist/skills/graphene/references/table.md +13 -14
- package/dist/skills/graphene/references/text-input.md +2 -1
- package/dist/ui/app.css +239 -340
- package/dist/ui/component-utilities/dataShaping.ts +484 -0
- package/dist/ui/component-utilities/dataSummary.ts +57 -0
- package/dist/ui/component-utilities/enrich.ts +763 -0
- package/dist/ui/component-utilities/format.ts +177 -0
- package/dist/ui/component-utilities/inputUtils.ts +44 -8
- package/dist/ui/component-utilities/theme.ts +200 -0
- package/dist/ui/component-utilities/themeStores.ts +21 -8
- package/dist/ui/component-utilities/types.ts +70 -0
- package/dist/ui/components/AreaChart.svelte +57 -105
- package/dist/ui/components/BarChart.svelte +71 -129
- package/dist/ui/components/BigValue.svelte +24 -40
- package/dist/ui/components/Column.svelte +10 -18
- package/dist/ui/components/DateRange.svelte +54 -21
- package/dist/ui/components/Dropdown.svelte +47 -26
- package/dist/ui/components/DropdownOption.svelte +1 -2
- package/dist/ui/components/ECharts.svelte +181 -67
- package/dist/ui/components/InlineDelta.svelte +50 -31
- package/dist/ui/components/LineChart.svelte +54 -125
- package/dist/ui/components/PieChart.svelte +27 -37
- package/dist/ui/components/QueryLoad.svelte +77 -45
- package/dist/ui/components/Row.svelte +2 -1
- package/dist/ui/components/ScatterPlot.svelte +52 -0
- package/dist/ui/components/Skeleton.svelte +32 -0
- package/dist/ui/components/Table.svelte +3 -2
- package/dist/ui/components/TableGroupRow.svelte +28 -36
- package/dist/ui/components/TableHarness.svelte +32 -0
- package/dist/ui/components/TableHeader.svelte +34 -59
- package/dist/ui/components/TableRow.svelte +14 -38
- package/dist/ui/components/TableSubtotalRow.svelte +18 -21
- package/dist/ui/components/TableTotalRow.svelte +27 -37
- package/dist/ui/components/TextInput.svelte +13 -12
- package/dist/ui/components/Value.svelte +25 -0
- package/dist/ui/components/_Table.svelte +72 -70
- package/dist/ui/internal/ChartGallery.svelte +527 -0
- package/dist/ui/internal/ErrorDisplay.svelte +22 -97
- package/dist/ui/internal/LocalApp.svelte +80 -17
- package/dist/ui/internal/PageNavGroup.svelte +269 -0
- package/dist/ui/internal/Sidebar.svelte +178 -0
- package/dist/ui/internal/SidebarToggle.svelte +47 -0
- package/dist/ui/internal/StyleGallery.svelte +244 -0
- package/dist/ui/internal/clientCache.ts +2 -2
- package/dist/ui/internal/pageInputs.svelte.js +292 -0
- package/dist/ui/internal/queryEngine.ts +102 -117
- package/dist/ui/internal/runSocket.ts +32 -12
- package/dist/ui/internal/sidebar.svelte.js +18 -0
- package/dist/ui/internal/telemetry.ts +51 -16
- package/dist/ui/internal/types.d.ts +7 -0
- package/dist/ui/web.js +28 -11
- package/package.json +36 -38
- package/dist/skills/graphene/references/area-chart.md +0 -95
- package/dist/skills/graphene/references/bar-chart.md +0 -112
- package/dist/skills/graphene/references/line-chart.md +0 -108
- package/dist/skills/graphene/references/pie-chart.md +0 -29
- package/dist/skills/graphene/references/value-formats.md +0 -104
- package/dist/ui/component-utilities/autoFormatting.js +0 -280
- package/dist/ui/component-utilities/builtInFormats.js +0 -481
- package/dist/ui/component-utilities/chartContext.js +0 -12
- package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
- package/dist/ui/component-utilities/checkInputs.js +0 -84
- package/dist/ui/component-utilities/convert.js +0 -15
- package/dist/ui/component-utilities/dateParsing.js +0 -56
- package/dist/ui/component-utilities/dropdownContext.ts +0 -1
- package/dist/ui/component-utilities/echarts.js +0 -252
- package/dist/ui/component-utilities/echartsThemes.js +0 -443
- package/dist/ui/component-utilities/formatTitle.js +0 -24
- package/dist/ui/component-utilities/formatting.js +0 -241
- package/dist/ui/component-utilities/getColumnExtents.js +0 -79
- package/dist/ui/component-utilities/getColumnSummary.js +0 -62
- package/dist/ui/component-utilities/getCompletedData.js +0 -122
- package/dist/ui/component-utilities/getDistinctCount.js +0 -7
- package/dist/ui/component-utilities/getDistinctValues.js +0 -15
- package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
- package/dist/ui/component-utilities/getSortedData.js +0 -9
- package/dist/ui/component-utilities/getStackPercentages.js +0 -45
- package/dist/ui/component-utilities/getStackedData.js +0 -19
- package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
- package/dist/ui/component-utilities/globalContexts.js +0 -1
- package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
- package/dist/ui/component-utilities/replaceNulls.js +0 -16
- package/dist/ui/component-utilities/tableUtils.ts +0 -107
- package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
- package/dist/ui/components/Area.svelte +0 -214
- package/dist/ui/components/Bar.svelte +0 -347
- package/dist/ui/components/Chart.svelte +0 -995
- package/dist/ui/components/Line.svelte +0 -227
- package/dist/ui/internal/NavSidebar.svelte +0 -396
- package/dist/ui/internal/theme.ts +0 -60
- package/dist/ui/public/inter-latin-ext.woff2 +0 -0
- package/dist/ui/public/inter-latin.woff2 +0 -0
|
@@ -1,45 +1,108 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
|
11
22
|
// Track compile errors from both initial load and subsequent HMR failures.
|
|
12
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
66
|
+
setErrorFor('compile', null)
|
|
67
|
+
}
|
|
68
|
+
await tick()
|
|
69
|
+
pageReadyResolve?.()
|
|
70
|
+
})
|
|
31
71
|
</script>
|
|
32
72
|
|
|
33
|
-
<
|
|
34
|
-
<
|
|
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
80
|
<h1 class="page-error-heading">Error loading page</h1>
|
|
37
81
|
<ErrorDisplay error={compileError} />
|
|
38
82
|
{:else if Page}
|
|
83
|
+
{#if pageMeta.title}
|
|
84
|
+
<h1>{pageMeta.title}</h1>
|
|
85
|
+
{/if}
|
|
39
86
|
<Page />
|
|
40
87
|
{/if}
|
|
41
88
|
</main>
|
|
42
89
|
|
|
43
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
|
+
|
|
44
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
|
+
}
|
|
45
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>
|