@leavepulse/ui 0.12.0
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/LICENSE +21 -0
- package/README.md +61 -0
- package/dist/component-names.d.ts +1 -0
- package/dist/components/LayoutCanvas.vue.d.ts +28 -0
- package/dist/components/LayoutNode.vue.d.ts +40 -0
- package/dist/components/LpAlert.vue.d.ts +44 -0
- package/dist/components/LpAppShell.vue.d.ts +83 -0
- package/dist/components/LpAutocomplete.vue.d.ts +50 -0
- package/dist/components/LpAvatar.vue.d.ts +20 -0
- package/dist/components/LpBadge.vue.d.ts +47 -0
- package/dist/components/LpBreadcrumbs.vue.d.ts +14 -0
- package/dist/components/LpButton.vue.d.ts +113 -0
- package/dist/components/LpCalendar.vue.d.ts +20 -0
- package/dist/components/LpCard.vue.d.ts +29 -0
- package/dist/components/LpCheckbox.vue.d.ts +28 -0
- package/dist/components/LpCodeBlock.vue.d.ts +39 -0
- package/dist/components/LpCommandPalette.vue.d.ts +42 -0
- package/dist/components/LpConfirmDialog.vue.d.ts +32 -0
- package/dist/components/LpContextMenu.vue.d.ts +30 -0
- package/dist/components/LpDatePicker.vue.d.ts +25 -0
- package/dist/components/LpDisclosure.vue.d.ts +37 -0
- package/dist/components/LpDivider.vue.d.ts +17 -0
- package/dist/components/LpDrawer.vue.d.ts +88 -0
- package/dist/components/LpDropdownMenu.vue.d.ts +23 -0
- package/dist/components/LpEmptyState.vue.d.ts +18 -0
- package/dist/components/LpFormField.vue.d.ts +21 -0
- package/dist/components/LpIcon.vue.d.ts +8 -0
- package/dist/components/LpInfraNode.vue.d.ts +21 -0
- package/dist/components/LpInput.vue.d.ts +68 -0
- package/dist/components/LpLaneNode.vue.d.ts +24 -0
- package/dist/components/LpLink.vue.d.ts +27 -0
- package/dist/components/LpLogViewer.vue.d.ts +92 -0
- package/dist/components/LpModal.vue.d.ts +43 -0
- package/dist/components/LpNotificationBell.vue.d.ts +51 -0
- package/dist/components/LpNumberField.vue.d.ts +14 -0
- package/dist/components/LpOtpInput.vue.d.ts +27 -0
- package/dist/components/LpPagination.vue.d.ts +25 -0
- package/dist/components/LpPasswordInput.vue.d.ts +28 -0
- package/dist/components/LpPhoneInput.vue.d.ts +37 -0
- package/dist/components/LpPopover.vue.d.ts +34 -0
- package/dist/components/LpProgress.vue.d.ts +12 -0
- package/dist/components/LpRadio.vue.d.ts +18 -0
- package/dist/components/LpRadioGroup.vue.d.ts +26 -0
- package/dist/components/LpScrollArea.vue.d.ts +24 -0
- package/dist/components/LpSegmented.vue.d.ts +24 -0
- package/dist/components/LpSelect.vue.d.ts +45 -0
- package/dist/components/LpServiceNode.vue.d.ts +14 -0
- package/dist/components/LpSidebar.vue.d.ts +85 -0
- package/dist/components/LpSidebarNav.vue.d.ts +34 -0
- package/dist/components/LpSkeleton.vue.d.ts +6 -0
- package/dist/components/LpSlider.vue.d.ts +23 -0
- package/dist/components/LpStat.vue.d.ts +22 -0
- package/dist/components/LpStepper.vue.d.ts +10 -0
- package/dist/components/LpSwitch.vue.d.ts +11 -0
- package/dist/components/LpTable.vue.d.ts +63 -0
- package/dist/components/LpTableOfContents.vue.d.ts +19 -0
- package/dist/components/LpTabs.vue.d.ts +45 -0
- package/dist/components/LpTextarea.vue.d.ts +17 -0
- package/dist/components/LpThemeSwitcher.vue.d.ts +40 -0
- package/dist/components/LpToaster.vue.d.ts +3 -0
- package/dist/components/LpTooltip.vue.d.ts +19 -0
- package/dist/components/LpTopologyCanvas.vue.d.ts +154 -0
- package/dist/components/LpUptimeBar.vue.d.ts +30 -0
- package/dist/components/codeHighlight.d.ts +8 -0
- package/dist/components/countries-data.d.ts +2 -0
- package/dist/components/countries.d.ts +20 -0
- package/dist/components/dropdown.d.ts +29 -0
- package/dist/components/sidebar.d.ts +18 -0
- package/dist/composables/useInputFilter.d.ts +14 -0
- package/dist/composables/usePillTransition.d.ts +9 -0
- package/dist/composables/useToast.d.ts +39 -0
- package/dist/countries-data-9rgqJWHo.js +203 -0
- package/dist/icons.d.ts +1 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +17600 -0
- package/dist/layout/registry.d.ts +26 -0
- package/dist/layout/tree.d.ts +75 -0
- package/dist/layout/useLayout.d.ts +39 -0
- package/dist/theme/config.d.ts +16 -0
- package/dist/theme/define.d.ts +12 -0
- package/dist/theme/presets.d.ts +20 -0
- package/dist/theme/tokens.d.ts +72 -0
- package/dist/theme/useTheme.d.ts +36 -0
- package/package.json +93 -0
- package/src/canvas.css +67 -0
- package/src/component-names.ts +65 -0
- package/src/components/LayoutCanvas.vue +76 -0
- package/src/components/LayoutNode.vue +248 -0
- package/src/components/LpAlert.vue +59 -0
- package/src/components/LpAppShell.vue +210 -0
- package/src/components/LpAutocomplete.vue +244 -0
- package/src/components/LpAvatar.vue +67 -0
- package/src/components/LpBadge.vue +32 -0
- package/src/components/LpBreadcrumbs.vue +35 -0
- package/src/components/LpButton.vue +111 -0
- package/src/components/LpCalendar.vue +134 -0
- package/src/components/LpCard.vue +71 -0
- package/src/components/LpCheckbox.vue +44 -0
- package/src/components/LpCodeBlock.vue +298 -0
- package/src/components/LpCommandPalette.vue +323 -0
- package/src/components/LpConfirmDialog.vue +49 -0
- package/src/components/LpContextMenu.vue +111 -0
- package/src/components/LpDatePicker.vue +117 -0
- package/src/components/LpDisclosure.vue +92 -0
- package/src/components/LpDivider.vue +20 -0
- package/src/components/LpDrawer.vue +402 -0
- package/src/components/LpDropdownMenu.vue +54 -0
- package/src/components/LpEmptyState.vue +21 -0
- package/src/components/LpFormField.vue +31 -0
- package/src/components/LpIcon.vue +49 -0
- package/src/components/LpInfraNode.vue +108 -0
- package/src/components/LpInput.vue +147 -0
- package/src/components/LpLaneNode.vue +48 -0
- package/src/components/LpLink.vue +48 -0
- package/src/components/LpLogViewer.vue +569 -0
- package/src/components/LpModal.vue +123 -0
- package/src/components/LpNotificationBell.vue +238 -0
- package/src/components/LpNumberField.vue +56 -0
- package/src/components/LpOtpInput.vue +81 -0
- package/src/components/LpPagination.vue +165 -0
- package/src/components/LpPasswordInput.vue +50 -0
- package/src/components/LpPhoneInput.vue +241 -0
- package/src/components/LpPopover.vue +42 -0
- package/src/components/LpProgress.vue +30 -0
- package/src/components/LpRadio.vue +28 -0
- package/src/components/LpRadioGroup.vue +43 -0
- package/src/components/LpScrollArea.vue +82 -0
- package/src/components/LpSegmented.vue +83 -0
- package/src/components/LpSelect.vue +0 -0
- package/src/components/LpServiceNode.vue +57 -0
- package/src/components/LpSidebar.vue +200 -0
- package/src/components/LpSidebarNav.vue +118 -0
- package/src/components/LpSkeleton.vue +16 -0
- package/src/components/LpSlider.vue +53 -0
- package/src/components/LpStat.vue +90 -0
- package/src/components/LpStepper.vue +46 -0
- package/src/components/LpSwitch.vue +19 -0
- package/src/components/LpTable.vue +252 -0
- package/src/components/LpTableOfContents.vue +183 -0
- package/src/components/LpTabs.vue +120 -0
- package/src/components/LpTextarea.vue +39 -0
- package/src/components/LpThemeSwitcher.vue +168 -0
- package/src/components/LpToaster.vue +216 -0
- package/src/components/LpTooltip.vue +30 -0
- package/src/components/LpTopologyCanvas.vue +370 -0
- package/src/components/LpUptimeBar.vue +126 -0
- package/src/components/codeHighlight.ts +137 -0
- package/src/components/countries-data.ts +207 -0
- package/src/components/countries.ts +67 -0
- package/src/components/dropdown.ts +64 -0
- package/src/components/sidebar.ts +26 -0
- package/src/composables/useInputFilter.ts +89 -0
- package/src/composables/usePillTransition.ts +19 -0
- package/src/composables/useToast.ts +92 -0
- package/src/icons.ts +11 -0
- package/src/index.css +21 -0
- package/src/index.ts +146 -0
- package/src/layout/registry.ts +44 -0
- package/src/layout/tree.ts +288 -0
- package/src/layout/useLayout.ts +134 -0
- package/src/nuxt.ts +22 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/define.ts +50 -0
- package/src/theme/presets.ts +279 -0
- package/src/theme/tokens.ts +151 -0
- package/src/theme/useTheme.ts +288 -0
- package/src/tokens/scrollbar.css +138 -0
- package/src/tokens/tokens.css +333 -0
- package/src/tokens/utilities.css +23 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export { default as LayoutCanvas } from "./components/LayoutCanvas.vue"
|
|
2
|
+
export { default as LpAlert } from "./components/LpAlert.vue"
|
|
3
|
+
export { default as LpAppShell } from "./components/LpAppShell.vue"
|
|
4
|
+
export { default as LpAutocomplete } from "./components/LpAutocomplete.vue"
|
|
5
|
+
export type { AutocompleteOption } from "./components/LpAutocomplete.vue"
|
|
6
|
+
export { default as LpAvatar } from "./components/LpAvatar.vue"
|
|
7
|
+
export { default as LpBadge } from "./components/LpBadge.vue"
|
|
8
|
+
export { default as LpBreadcrumbs } from "./components/LpBreadcrumbs.vue"
|
|
9
|
+
export type { Crumb } from "./components/LpBreadcrumbs.vue"
|
|
10
|
+
export { default as LpButton } from "./components/LpButton.vue"
|
|
11
|
+
export { default as LpCalendar } from "./components/LpCalendar.vue"
|
|
12
|
+
export { default as LpCard } from "./components/LpCard.vue"
|
|
13
|
+
export { default as LpCheckbox } from "./components/LpCheckbox.vue"
|
|
14
|
+
export { default as LpCodeBlock } from "./components/LpCodeBlock.vue"
|
|
15
|
+
export type { CodeLang } from "./components/codeHighlight"
|
|
16
|
+
export { default as LpCommandPalette } from "./components/LpCommandPalette.vue"
|
|
17
|
+
export type { Command } from "./components/LpCommandPalette.vue"
|
|
18
|
+
export { default as LpConfirmDialog } from "./components/LpConfirmDialog.vue"
|
|
19
|
+
export { default as LpContextMenu } from "./components/LpContextMenu.vue"
|
|
20
|
+
export type { ContextMenuItemDef } from "./components/LpContextMenu.vue"
|
|
21
|
+
export { default as LpDatePicker } from "./components/LpDatePicker.vue"
|
|
22
|
+
export { default as LpDisclosure } from "./components/LpDisclosure.vue"
|
|
23
|
+
export { default as LpDivider } from "./components/LpDivider.vue"
|
|
24
|
+
export { default as LpDrawer } from "./components/LpDrawer.vue"
|
|
25
|
+
export { default as LpDropdownMenu } from "./components/LpDropdownMenu.vue"
|
|
26
|
+
export type { MenuItem } from "./components/LpDropdownMenu.vue"
|
|
27
|
+
export { default as LpEmptyState } from "./components/LpEmptyState.vue"
|
|
28
|
+
export { default as LpFormField } from "./components/LpFormField.vue"
|
|
29
|
+
export { default as LpIcon } from "./components/LpIcon.vue"
|
|
30
|
+
export { default as LpInfraNode } from "./components/LpInfraNode.vue"
|
|
31
|
+
export type { InfraNodeData } from "./components/LpInfraNode.vue"
|
|
32
|
+
export { default as LpInput } from "./components/LpInput.vue"
|
|
33
|
+
export { default as LpLink } from "./components/LpLink.vue"
|
|
34
|
+
export { default as LpLogViewer } from "./components/LpLogViewer.vue"
|
|
35
|
+
export type { LogLevel, LogLine } from "./components/LpLogViewer.vue"
|
|
36
|
+
export { default as LpModal } from "./components/LpModal.vue"
|
|
37
|
+
export { default as LpNotificationBell } from "./components/LpNotificationBell.vue"
|
|
38
|
+
export type { NotificationItem } from "./components/LpNotificationBell.vue"
|
|
39
|
+
export { default as LpNumberField } from "./components/LpNumberField.vue"
|
|
40
|
+
export { default as LpOtpInput } from "./components/LpOtpInput.vue"
|
|
41
|
+
export { default as LpPagination } from "./components/LpPagination.vue"
|
|
42
|
+
export { default as LpPasswordInput } from "./components/LpPasswordInput.vue"
|
|
43
|
+
export { default as LpPhoneInput } from "./components/LpPhoneInput.vue"
|
|
44
|
+
export type { PhoneDetail } from "./components/LpPhoneInput.vue"
|
|
45
|
+
export { flagEmoji, loadCountries, matchCountryByValue } from "./components/countries"
|
|
46
|
+
export type { Country } from "./components/countries"
|
|
47
|
+
export { default as LpPopover } from "./components/LpPopover.vue"
|
|
48
|
+
export { default as LpProgress } from "./components/LpProgress.vue"
|
|
49
|
+
export { default as LpRadio } from "./components/LpRadio.vue"
|
|
50
|
+
export { default as LpRadioGroup } from "./components/LpRadioGroup.vue"
|
|
51
|
+
export type { RadioOption } from "./components/LpRadioGroup.vue"
|
|
52
|
+
export { default as LpScrollArea } from "./components/LpScrollArea.vue"
|
|
53
|
+
export { default as LpSegmented } from "./components/LpSegmented.vue"
|
|
54
|
+
export type { SegmentedOption } from "./components/LpSegmented.vue"
|
|
55
|
+
export { default as LpSelect } from "./components/LpSelect.vue"
|
|
56
|
+
export type { SelectOption, SelectValue } from "./components/LpSelect.vue"
|
|
57
|
+
export { default as LpServiceNode } from "./components/LpServiceNode.vue"
|
|
58
|
+
export type { ServiceNodeData } from "./components/LpServiceNode.vue"
|
|
59
|
+
export { default as LpSidebar } from "./components/LpSidebar.vue"
|
|
60
|
+
export type { SidebarItem, SidebarSection } from "./components/LpSidebar.vue"
|
|
61
|
+
export { default as LpSkeleton } from "./components/LpSkeleton.vue"
|
|
62
|
+
export { default as LpSlider } from "./components/LpSlider.vue"
|
|
63
|
+
export { default as LpStat } from "./components/LpStat.vue"
|
|
64
|
+
export { default as LpStepper } from "./components/LpStepper.vue"
|
|
65
|
+
export type { Step } from "./components/LpStepper.vue"
|
|
66
|
+
export { default as LpSwitch } from "./components/LpSwitch.vue"
|
|
67
|
+
export { default as LpTable } from "./components/LpTable.vue"
|
|
68
|
+
export type { SortState, TableColumn } from "./components/LpTable.vue"
|
|
69
|
+
export { default as LpTableOfContents } from "./components/LpTableOfContents.vue"
|
|
70
|
+
export type { TocLink } from "./components/LpTableOfContents.vue"
|
|
71
|
+
export { default as LpTabs } from "./components/LpTabs.vue"
|
|
72
|
+
export type { TabItem } from "./components/LpTabs.vue"
|
|
73
|
+
export { default as LpTextarea } from "./components/LpTextarea.vue"
|
|
74
|
+
export { default as LpThemeSwitcher } from "./components/LpThemeSwitcher.vue"
|
|
75
|
+
export { default as LpToaster } from "./components/LpToaster.vue"
|
|
76
|
+
export { default as LpTooltip } from "./components/LpTooltip.vue"
|
|
77
|
+
export { default as LpTopologyCanvas } from "./components/LpTopologyCanvas.vue"
|
|
78
|
+
export type {
|
|
79
|
+
CanvasViewport,
|
|
80
|
+
EdgeCategory,
|
|
81
|
+
EdgeObserved,
|
|
82
|
+
TopologyEdge,
|
|
83
|
+
TopologyLane,
|
|
84
|
+
TopologyNode,
|
|
85
|
+
} from "./components/LpTopologyCanvas.vue"
|
|
86
|
+
export { default as LpUptimeBar } from "./components/LpUptimeBar.vue"
|
|
87
|
+
export type { UptimeSegment, UptimeStatus } from "./components/LpUptimeBar.vue"
|
|
88
|
+
|
|
89
|
+
export {
|
|
90
|
+
addLeaf,
|
|
91
|
+
addTab,
|
|
92
|
+
countBlocks,
|
|
93
|
+
countLeaves,
|
|
94
|
+
deserializeLayout,
|
|
95
|
+
makeLayout,
|
|
96
|
+
moveLeaf,
|
|
97
|
+
removeBlock,
|
|
98
|
+
removeLeaf,
|
|
99
|
+
reorderTab,
|
|
100
|
+
resizeAt,
|
|
101
|
+
setActiveTab,
|
|
102
|
+
serializeLayout,
|
|
103
|
+
} from "./layout/tree"
|
|
104
|
+
export type { Dir, LayoutNode, Leaf, Side, Split } from "./layout/tree"
|
|
105
|
+
export { blockTitle, defineBlocks } from "./layout/registry"
|
|
106
|
+
export type { BlockDef, BlockRegistry } from "./layout/registry"
|
|
107
|
+
export { useLayout } from "./layout/useLayout"
|
|
108
|
+
export type { UseLayout, UseLayoutOptions } from "./layout/useLayout"
|
|
109
|
+
|
|
110
|
+
// theme engine — themes are JSON (TokenSet), applied at runtime
|
|
111
|
+
export {
|
|
112
|
+
applyTheme,
|
|
113
|
+
applyThemeWithTransition,
|
|
114
|
+
bootstrapTheme,
|
|
115
|
+
parseTheme,
|
|
116
|
+
serializeTheme,
|
|
117
|
+
themeToCssRule,
|
|
118
|
+
themeToCssVars,
|
|
119
|
+
useTheme,
|
|
120
|
+
} from "./theme/useTheme"
|
|
121
|
+
export type { RevealOrigin } from "./theme/useTheme"
|
|
122
|
+
export { DEFAULT_SURFACE } from "./theme/tokens"
|
|
123
|
+
export { defineTheme } from "./theme/define"
|
|
124
|
+
export type { ThemeOverrides } from "./theme/define"
|
|
125
|
+
export type {
|
|
126
|
+
ColorTokens,
|
|
127
|
+
DensityTokens,
|
|
128
|
+
FontTokens,
|
|
129
|
+
ShapeTokens,
|
|
130
|
+
SurfaceTokens,
|
|
131
|
+
TokenSet,
|
|
132
|
+
} from "./theme/tokens"
|
|
133
|
+
export { amber, dark, leavepulse, light, lime, nord, presets, rose, violet } from "./theme/presets"
|
|
134
|
+
export type { PresetName } from "./theme/presets"
|
|
135
|
+
export { parseConfig, serializeConfig, UI_CONFIG_VERSION } from "./theme/config"
|
|
136
|
+
export type { UiConfig } from "./theme/config"
|
|
137
|
+
|
|
138
|
+
export { useInputFilter } from "./composables/useInputFilter"
|
|
139
|
+
export type { UseInputFilterOptions } from "./composables/useInputFilter"
|
|
140
|
+
export { useToast } from "./composables/useToast"
|
|
141
|
+
export type {
|
|
142
|
+
ToastAction,
|
|
143
|
+
ToastItem,
|
|
144
|
+
ToastOptions,
|
|
145
|
+
ToastVariant,
|
|
146
|
+
} from "./composables/useToast"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Component } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Block registry — the consumer's catalogue of what a layout cell can hold.
|
|
5
|
+
*
|
|
6
|
+
* The layout tree stores only block ids (plain strings, serializable). The
|
|
7
|
+
* registry maps each id to how it should render and chrome: the component, a
|
|
8
|
+
* human title for the tab/header, an optional icon, and whether the user may
|
|
9
|
+
* close it. LayoutCanvas reads this so it can draw tab bars + headers itself —
|
|
10
|
+
* the consumer supplies the catalogue, not per-cell wiring.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface BlockDef {
|
|
14
|
+
/**
|
|
15
|
+
* Content component for this block. Optional: when the consumer renders block
|
|
16
|
+
* content via the canvas's #block slot (keyed by id), the registry is used
|
|
17
|
+
* only for tab chrome (title/icon) and no component is needed here.
|
|
18
|
+
*/
|
|
19
|
+
component?: Component
|
|
20
|
+
/** Human label shown on the tab / cell header. Defaults to the id. */
|
|
21
|
+
title?: string
|
|
22
|
+
/** Optional iconify name (e.g. "lucide:activity") for the tab. */
|
|
23
|
+
icon?: string
|
|
24
|
+
/** Whether the user may close this block. Defaults to true. */
|
|
25
|
+
closable?: boolean
|
|
26
|
+
/** Props passed to the component. */
|
|
27
|
+
props?: Record<string, unknown>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type BlockRegistry = Record<string, BlockDef>
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Identity helper that pins the key type so callers get autocomplete on block
|
|
34
|
+
* ids elsewhere (e.g. when seeding a layout). Purely for inference — it returns
|
|
35
|
+
* the object unchanged.
|
|
36
|
+
*/
|
|
37
|
+
export function defineBlocks<const T extends BlockRegistry>(blocks: T): T {
|
|
38
|
+
return blocks
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Resolve a block's display title, falling back to its id. */
|
|
42
|
+
export function blockTitle(registry: BlockRegistry, id: string): string {
|
|
43
|
+
return registry[id]?.title ?? id
|
|
44
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { reactive } from "vue"
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Layout tree model for LayoutCanvas.
|
|
5
|
+
*
|
|
6
|
+
* A layout is a tree of splits (row/col containers) and leaves (blocks).
|
|
7
|
+
* Sizes are flex fractions among siblings, so the layout is resolution-
|
|
8
|
+
* independent and serializes cleanly to JSON (persist to disk / config).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type Dir = "row" | "col"
|
|
12
|
+
export type Side = "left" | "right" | "top" | "bottom" | "center"
|
|
13
|
+
|
|
14
|
+
export interface Leaf {
|
|
15
|
+
kind: "leaf"
|
|
16
|
+
id: string
|
|
17
|
+
/**
|
|
18
|
+
* Block ids stacked as tabs in this cell. A plain cell is a single-element
|
|
19
|
+
* stack; dropping onto a cell's center adds another tab. The consumer maps
|
|
20
|
+
* each id to content via the canvas slot.
|
|
21
|
+
*/
|
|
22
|
+
blocks: string[]
|
|
23
|
+
/** Index into `blocks` of the visible tab. */
|
|
24
|
+
active: number
|
|
25
|
+
/** Flex fraction among siblings. */
|
|
26
|
+
size: number
|
|
27
|
+
}
|
|
28
|
+
export interface Split {
|
|
29
|
+
kind: "split"
|
|
30
|
+
id: string
|
|
31
|
+
dir: Dir
|
|
32
|
+
size: number
|
|
33
|
+
children: LayoutNode[]
|
|
34
|
+
}
|
|
35
|
+
export type LayoutNode = Leaf | Split
|
|
36
|
+
|
|
37
|
+
let uid = 0
|
|
38
|
+
const nid = () => `lp-n${uid++}`
|
|
39
|
+
|
|
40
|
+
function leaf(block: string): Leaf {
|
|
41
|
+
return { kind: "leaf", id: nid(), blocks: [block], active: 0, size: 1 }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Build a single-row layout from a flat list of block ids (one cell each). */
|
|
45
|
+
export function makeLayout(blocks: string[]): Split {
|
|
46
|
+
return reactive<Split>({
|
|
47
|
+
kind: "split",
|
|
48
|
+
id: nid(),
|
|
49
|
+
dir: "row",
|
|
50
|
+
size: 1,
|
|
51
|
+
children: blocks.map<Leaf>(leaf),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function findParent(node: Split, childId: string): Split | null {
|
|
56
|
+
for (const c of node.children) {
|
|
57
|
+
if (c.id === childId) return node
|
|
58
|
+
if (c.kind === "split") {
|
|
59
|
+
const r = findParent(c, childId)
|
|
60
|
+
if (r) return r
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function findLeaf(node: LayoutNode, id: string): Leaf | null {
|
|
67
|
+
if (node.kind === "leaf") return node.id === id ? node : null
|
|
68
|
+
for (const c of node.children) {
|
|
69
|
+
const r = findLeaf(c, id)
|
|
70
|
+
if (r) return r
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function countLeaves(node: LayoutNode): number {
|
|
76
|
+
if (node.kind === "leaf") return 1
|
|
77
|
+
return node.children.reduce((s, c) => s + countLeaves(c), 0)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Total blocks across all cells (a tabbed cell counts each of its tabs). */
|
|
81
|
+
export function countBlocks(node: LayoutNode): number {
|
|
82
|
+
if (node.kind === "leaf") return node.blocks.length
|
|
83
|
+
return node.children.reduce((s, c) => s + countBlocks(c), 0)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detach(root: Split, id: string): void {
|
|
87
|
+
const parent = findParent(root, id)
|
|
88
|
+
if (!parent) return
|
|
89
|
+
const idx = parent.children.findIndex((c) => c.id === id)
|
|
90
|
+
parent.children.splice(idx, 1)
|
|
91
|
+
collapse(root, parent)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A container left with a single child collapses into that child. */
|
|
95
|
+
function collapse(root: Split, parent: Split): void {
|
|
96
|
+
if (parent === root || parent.children.length !== 1) return
|
|
97
|
+
const grand = findParent(root, parent.id)
|
|
98
|
+
if (!grand) return
|
|
99
|
+
const only = parent.children[0]
|
|
100
|
+
only.size = parent.size
|
|
101
|
+
const pIdx = grand.children.findIndex((c) => c.id === parent.id)
|
|
102
|
+
grand.children.splice(pIdx, 1, only)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Remove an entire cell (with all its tabs). */
|
|
106
|
+
export function removeLeaf(root: Split, id: string): void {
|
|
107
|
+
if (countLeaves(root) <= 1) return // keep at least one cell
|
|
108
|
+
detach(root, id)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove one tab from a cell. Removing the last tab removes the cell itself
|
|
113
|
+
* (unless it's the only block left in the whole layout, which is kept).
|
|
114
|
+
*/
|
|
115
|
+
export function removeBlock(root: Split, leafId: string, index: number): void {
|
|
116
|
+
const target = findLeaf(root, leafId)
|
|
117
|
+
if (!target || index < 0 || index >= target.blocks.length) return
|
|
118
|
+
if (target.blocks.length > 1) {
|
|
119
|
+
target.blocks.splice(index, 1)
|
|
120
|
+
if (target.active >= target.blocks.length) target.active = target.blocks.length - 1
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
// Last tab in this cell → drop the cell, but never empty the whole layout.
|
|
124
|
+
if (countBlocks(root) <= 1) return
|
|
125
|
+
detach(root, leafId)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function addLeaf(root: Split, block: string, side: Side = "right"): void {
|
|
129
|
+
const targetId = root.children[root.children.length - 1]?.id
|
|
130
|
+
if (!targetId) {
|
|
131
|
+
root.children.push(leaf(block))
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
if (side === "center") {
|
|
135
|
+
addTab(root, targetId, block)
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
insertBeside(root, leaf(block), targetId, side)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Append a block as a new tab in the target cell and focus it. */
|
|
142
|
+
export function addTab(root: Split, leafId: string, block: string): void {
|
|
143
|
+
const target = findLeaf(root, leafId)
|
|
144
|
+
if (!target) return
|
|
145
|
+
target.blocks.push(block)
|
|
146
|
+
target.active = target.blocks.length - 1
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Switch the visible tab of a cell. */
|
|
150
|
+
export function setActiveTab(root: Split, leafId: string, index: number): void {
|
|
151
|
+
const target = findLeaf(root, leafId)
|
|
152
|
+
if (!target || index < 0 || index >= target.blocks.length) return
|
|
153
|
+
target.active = index
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Reorder a tab within its cell, moving the block at `from` to `to`. The same
|
|
158
|
+
* block stays visible afterwards (active follows the moved block by identity,
|
|
159
|
+
* not by index). Always available — not gated by edit mode.
|
|
160
|
+
*/
|
|
161
|
+
export function reorderTab(root: Split, leafId: string, from: number, to: number): void {
|
|
162
|
+
const target = findLeaf(root, leafId)
|
|
163
|
+
if (!target) return
|
|
164
|
+
const n = target.blocks.length
|
|
165
|
+
if (from < 0 || from >= n || to < 0 || to >= n || from === to) return
|
|
166
|
+
const activeBlock = target.blocks[target.active]
|
|
167
|
+
const [moved] = target.blocks.splice(from, 1)
|
|
168
|
+
target.blocks.splice(to, 0, moved)
|
|
169
|
+
target.active = target.blocks.indexOf(activeBlock)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function insertBeside(
|
|
173
|
+
root: Split,
|
|
174
|
+
moving: Leaf,
|
|
175
|
+
targetId: string,
|
|
176
|
+
side: Side,
|
|
177
|
+
): void {
|
|
178
|
+
const parent = findParent(root, targetId)
|
|
179
|
+
if (!parent) return
|
|
180
|
+
const idx = parent.children.findIndex((c) => c.id === targetId)
|
|
181
|
+
const target = parent.children[idx]
|
|
182
|
+
|
|
183
|
+
const wantDir: Dir = side === "left" || side === "right" ? "row" : "col"
|
|
184
|
+
const before = side === "left" || side === "top"
|
|
185
|
+
|
|
186
|
+
if (parent.dir === wantDir) {
|
|
187
|
+
moving.size = target.size / 2
|
|
188
|
+
target.size = target.size / 2
|
|
189
|
+
parent.children.splice(before ? idx : idx + 1, 0, moving)
|
|
190
|
+
} else {
|
|
191
|
+
const wrap: Split = {
|
|
192
|
+
kind: "split",
|
|
193
|
+
id: nid(),
|
|
194
|
+
dir: wantDir,
|
|
195
|
+
size: target.size,
|
|
196
|
+
children: before
|
|
197
|
+
? [{ ...moving, size: 1 }, { ...target, size: 1 }]
|
|
198
|
+
: [{ ...target, size: 1 }, { ...moving, size: 1 }],
|
|
199
|
+
}
|
|
200
|
+
parent.children.splice(idx, 1, wrap)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Move an existing cell to a target. An edge side splits beside the target;
|
|
206
|
+
* `center` merges the moving cell's tabs into the target cell (and focuses
|
|
207
|
+
* the first moved tab).
|
|
208
|
+
*/
|
|
209
|
+
export function moveLeaf(
|
|
210
|
+
root: Split,
|
|
211
|
+
movingId: string,
|
|
212
|
+
targetId: string,
|
|
213
|
+
side: Side,
|
|
214
|
+
): void {
|
|
215
|
+
if (movingId === targetId) return
|
|
216
|
+
const moving = findLeaf(root, movingId)
|
|
217
|
+
if (!moving) return
|
|
218
|
+
|
|
219
|
+
if (side === "center") {
|
|
220
|
+
const target = findLeaf(root, targetId)
|
|
221
|
+
if (!target) return
|
|
222
|
+
const merged = moving.blocks
|
|
223
|
+
detach(root, movingId)
|
|
224
|
+
target.active = target.blocks.length
|
|
225
|
+
target.blocks.push(...merged)
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
detach(root, movingId)
|
|
230
|
+
moving.size = 1
|
|
231
|
+
insertBeside(root, moving, targetId, side)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Splitter drag: redistribute size between two adjacent siblings. */
|
|
235
|
+
export function resizeAt(parent: Split, index: number, deltaFraction: number): void {
|
|
236
|
+
const a = parent.children[index]
|
|
237
|
+
const b = parent.children[index + 1]
|
|
238
|
+
if (!a || !b) return
|
|
239
|
+
const total = a.size + b.size
|
|
240
|
+
const min = total * 0.12
|
|
241
|
+
const na = Math.max(min, Math.min(total - min, a.size + deltaFraction * total))
|
|
242
|
+
a.size = na
|
|
243
|
+
b.size = total - na
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── serialization ────────────────────────────────────────────
|
|
247
|
+
interface SerializedLeaf {
|
|
248
|
+
kind: "leaf"
|
|
249
|
+
blocks: string[]
|
|
250
|
+
active: number
|
|
251
|
+
size: number
|
|
252
|
+
/** Legacy single-block form (pre-tabs); hydrated into `blocks`. */
|
|
253
|
+
block?: string
|
|
254
|
+
}
|
|
255
|
+
interface SerializedSplit {
|
|
256
|
+
kind: "split"
|
|
257
|
+
dir: Dir
|
|
258
|
+
size: number
|
|
259
|
+
children: SerializedNode[]
|
|
260
|
+
}
|
|
261
|
+
type SerializedNode = SerializedLeaf | SerializedSplit
|
|
262
|
+
|
|
263
|
+
export function serializeLayout(root: Split): SerializedNode {
|
|
264
|
+
const strip = (n: LayoutNode): SerializedNode =>
|
|
265
|
+
n.kind === "leaf"
|
|
266
|
+
? { kind: "leaf", blocks: [...n.blocks], active: n.active, size: n.size }
|
|
267
|
+
: { kind: "split", dir: n.dir, size: n.size, children: n.children.map(strip) }
|
|
268
|
+
return strip(root) as SerializedSplit
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function deserializeLayout(data: SerializedNode): Split {
|
|
272
|
+
const hydrate = (n: SerializedNode): LayoutNode => {
|
|
273
|
+
if (n.kind === "leaf") {
|
|
274
|
+
// Accept both the tabbed form and the legacy single-`block` form.
|
|
275
|
+
const blocks = n.blocks ?? (n.block != null ? [n.block] : [])
|
|
276
|
+
const active = Math.min(Math.max(n.active ?? 0, 0), Math.max(blocks.length - 1, 0))
|
|
277
|
+
return { kind: "leaf", id: nid(), blocks, active, size: n.size ?? 1 }
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
kind: "split",
|
|
281
|
+
id: nid(),
|
|
282
|
+
dir: n.dir,
|
|
283
|
+
size: n.size ?? 1,
|
|
284
|
+
children: n.children.map(hydrate),
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return reactive(hydrate(data) as Split)
|
|
288
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { computed, ref, shallowRef, type Ref } from "vue"
|
|
2
|
+
import {
|
|
3
|
+
addLeaf,
|
|
4
|
+
addTab,
|
|
5
|
+
countBlocks,
|
|
6
|
+
deserializeLayout,
|
|
7
|
+
makeLayout,
|
|
8
|
+
moveLeaf,
|
|
9
|
+
removeBlock,
|
|
10
|
+
removeLeaf,
|
|
11
|
+
reorderTab,
|
|
12
|
+
resizeAt,
|
|
13
|
+
setActiveTab,
|
|
14
|
+
serializeLayout,
|
|
15
|
+
type Side,
|
|
16
|
+
type Split,
|
|
17
|
+
} from "./tree"
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* useLayout — stateful controller around the layout tree.
|
|
21
|
+
*
|
|
22
|
+
* tree.ts is a pile of pure-ish mutators; this composable is the ergonomic
|
|
23
|
+
* surface a consumer actually wires to UI: it owns the reactive tree, snapshots
|
|
24
|
+
* every structural edit for undo/redo, and exposes save/load/reset against a
|
|
25
|
+
* serialized form. The canvas calls the same mutators for drag/drop/resize, so
|
|
26
|
+
* those go through `record()` too and land in history.
|
|
27
|
+
*
|
|
28
|
+
* History holds serialized snapshots (plain JSON), not live trees, so an undo
|
|
29
|
+
* can't smuggle stale reactive nodes back in.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
export interface UseLayoutOptions {
|
|
33
|
+
/** Initial block ids, left-to-right, when no saved layout is loaded. */
|
|
34
|
+
initial: string[]
|
|
35
|
+
/** Max undo depth. */
|
|
36
|
+
historyLimit?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface UseLayout {
|
|
40
|
+
/** The live reactive tree to hand to <LayoutCanvas v-model>. */
|
|
41
|
+
layout: Ref<Split>
|
|
42
|
+
/** Block count across all tabs, handy for "N blocks" labels and guards. */
|
|
43
|
+
count: Ref<number>
|
|
44
|
+
canUndo: Ref<boolean>
|
|
45
|
+
canRedo: Ref<boolean>
|
|
46
|
+
/** Add a block: a new cell on an edge, or a tab on the last cell (center). */
|
|
47
|
+
add: (block: string, side?: Side) => void
|
|
48
|
+
/** Add a block as a tab in a specific cell. */
|
|
49
|
+
addTab: (leafId: string, block: string) => void
|
|
50
|
+
/** Switch the visible tab of a cell. */
|
|
51
|
+
setActive: (leafId: string, index: number) => void
|
|
52
|
+
/** Reorder a tab within its cell (always available, not edit-gated). */
|
|
53
|
+
reorderTab: (leafId: string, from: number, to: number) => void
|
|
54
|
+
/** Remove one tab (drops the cell when it was the last tab). */
|
|
55
|
+
removeBlock: (leafId: string, index: number) => void
|
|
56
|
+
/** Remove an entire cell with all its tabs. */
|
|
57
|
+
remove: (leafId: string) => void
|
|
58
|
+
move: (movingId: string, targetId: string, side: Side) => void
|
|
59
|
+
resize: (parent: Split, index: number, deltaFraction: number) => void
|
|
60
|
+
undo: () => void
|
|
61
|
+
redo: () => void
|
|
62
|
+
/** Replace the whole layout from a flat block list. */
|
|
63
|
+
reset: (blocks?: string[]) => void
|
|
64
|
+
/** Serialize to JSON-safe data for persistence. */
|
|
65
|
+
serialize: () => unknown
|
|
66
|
+
/** Replace the layout from previously serialized data. */
|
|
67
|
+
hydrate: (data: unknown) => void
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function useLayout(options: UseLayoutOptions): UseLayout {
|
|
71
|
+
const limit = options.historyLimit ?? 50
|
|
72
|
+
const layout = shallowRef<Split>(makeLayout(options.initial))
|
|
73
|
+
const count = computed(() => countBlocks(layout.value))
|
|
74
|
+
|
|
75
|
+
const past = ref<string[]>([])
|
|
76
|
+
const future = ref<string[]>([])
|
|
77
|
+
const canUndo = computed(() => past.value.length > 0)
|
|
78
|
+
const canRedo = computed(() => future.value.length > 0)
|
|
79
|
+
|
|
80
|
+
const snapshot = () => JSON.stringify(serializeLayout(layout.value))
|
|
81
|
+
|
|
82
|
+
// Capture the pre-edit state, run the structural change, drop any redo branch.
|
|
83
|
+
function record(mutate: () => void) {
|
|
84
|
+
const before = snapshot()
|
|
85
|
+
mutate()
|
|
86
|
+
past.value.push(before)
|
|
87
|
+
if (past.value.length > limit) past.value.shift()
|
|
88
|
+
future.value = []
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function restore(serialized: string) {
|
|
92
|
+
layout.value = deserializeLayout(JSON.parse(serialized))
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
layout,
|
|
97
|
+
count,
|
|
98
|
+
canUndo,
|
|
99
|
+
canRedo,
|
|
100
|
+
add: (block, side) => record(() => addLeaf(layout.value, block, side)),
|
|
101
|
+
addTab: (leafId, block) => record(() => addTab(layout.value, leafId, block)),
|
|
102
|
+
setActive: (leafId, index) => setActiveTab(layout.value, leafId, index),
|
|
103
|
+
reorderTab: (leafId, from, to) => record(() => reorderTab(layout.value, leafId, from, to)),
|
|
104
|
+
removeBlock: (leafId, index) => record(() => removeBlock(layout.value, leafId, index)),
|
|
105
|
+
remove: (leafId) => record(() => removeLeaf(layout.value, leafId)),
|
|
106
|
+
move: (movingId, targetId, side) =>
|
|
107
|
+
record(() => moveLeaf(layout.value, movingId, targetId, side)),
|
|
108
|
+
// Resize fires continuously during a divider drag; recording every frame
|
|
109
|
+
// would flood history, so it mutates without a snapshot. (A consumer that
|
|
110
|
+
// wants resize in history can wrap a single record() around the gesture.)
|
|
111
|
+
resize: (parent, index, delta) => resizeAt(parent, index, delta),
|
|
112
|
+
undo: () => {
|
|
113
|
+
const prev = past.value.pop()
|
|
114
|
+
if (prev === undefined) return
|
|
115
|
+
future.value.push(snapshot())
|
|
116
|
+
restore(prev)
|
|
117
|
+
},
|
|
118
|
+
redo: () => {
|
|
119
|
+
const next = future.value.pop()
|
|
120
|
+
if (next === undefined) return
|
|
121
|
+
past.value.push(snapshot())
|
|
122
|
+
restore(next)
|
|
123
|
+
},
|
|
124
|
+
reset: (blocks) =>
|
|
125
|
+
record(() => {
|
|
126
|
+
layout.value = makeLayout(blocks ?? options.initial)
|
|
127
|
+
}),
|
|
128
|
+
serialize: () => serializeLayout(layout.value),
|
|
129
|
+
hydrate: (data) =>
|
|
130
|
+
record(() => {
|
|
131
|
+
layout.value = deserializeLayout(data as Parameters<typeof deserializeLayout>[0])
|
|
132
|
+
}),
|
|
133
|
+
}
|
|
134
|
+
}
|
package/src/nuxt.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Nuxt module — auto-registers every kit component globally so consuming apps
|
|
3
|
+
* use <LpButton> / <LpThemeSwitcher> / … with no per-app component list to
|
|
4
|
+
* maintain. Add it to a Nuxt app's `modules`:
|
|
5
|
+
*
|
|
6
|
+
* export default defineNuxtConfig({ modules: ["@leavepulse/ui/nuxt"] })
|
|
7
|
+
*
|
|
8
|
+
* The list comes from the generated component-names file (kept in sync with the
|
|
9
|
+
* actual components at build), so a NEW component is available everywhere with
|
|
10
|
+
* no consumer change.
|
|
11
|
+
*/
|
|
12
|
+
import { addComponent, defineNuxtModule } from "@nuxt/kit"
|
|
13
|
+
import { COMPONENT_NAMES } from "./component-names"
|
|
14
|
+
|
|
15
|
+
export default defineNuxtModule({
|
|
16
|
+
meta: { name: "@leavepulse/ui", configKey: "leavepulseUi" },
|
|
17
|
+
setup() {
|
|
18
|
+
for (const name of COMPONENT_NAMES) {
|
|
19
|
+
addComponent({ name, export: name, filePath: "@leavepulse/ui" })
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* UiConfig — the canonical, complete UI-state snapshot for a LeavePulse app.
|
|
3
|
+
*
|
|
4
|
+
* Deliberately broader than the launcher's old theme config: it carries the
|
|
5
|
+
* THEME *and* the LAYOUT (block placement) in one serializable object, plus a
|
|
6
|
+
* schema version for forward migration. This is the source of truth apps load
|
|
7
|
+
* from / save to disk.
|
|
8
|
+
*/
|
|
9
|
+
import type { Split } from "../layout/tree"
|
|
10
|
+
import { serializeLayout, deserializeLayout } from "../layout/tree"
|
|
11
|
+
import type { TokenSet } from "./tokens"
|
|
12
|
+
import { parseTheme, serializeTheme } from "./useTheme"
|
|
13
|
+
|
|
14
|
+
export const UI_CONFIG_VERSION = 1
|
|
15
|
+
|
|
16
|
+
export interface UiConfig {
|
|
17
|
+
version: number
|
|
18
|
+
theme: TokenSet
|
|
19
|
+
/** Optional — apps without a composable layout omit it. */
|
|
20
|
+
layout?: Split
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SerializedUiConfig {
|
|
24
|
+
version: number
|
|
25
|
+
theme: TokenSet
|
|
26
|
+
layout?: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function serializeConfig(config: UiConfig): string {
|
|
30
|
+
const out: SerializedUiConfig = {
|
|
31
|
+
version: UI_CONFIG_VERSION,
|
|
32
|
+
theme: JSON.parse(serializeTheme(config.theme)),
|
|
33
|
+
layout: config.layout ? serializeLayout(config.layout) : undefined,
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify(out)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseConfig(json: string): UiConfig {
|
|
39
|
+
const raw = JSON.parse(json) as SerializedUiConfig
|
|
40
|
+
if (!raw || typeof raw !== "object") throw new Error("Invalid UiConfig")
|
|
41
|
+
return {
|
|
42
|
+
version: raw.version ?? UI_CONFIG_VERSION,
|
|
43
|
+
theme: parseTheme(JSON.stringify(raw.theme)),
|
|
44
|
+
layout: raw.layout
|
|
45
|
+
? deserializeLayout(raw.layout as Parameters<typeof deserializeLayout>[0])
|
|
46
|
+
: undefined,
|
|
47
|
+
}
|
|
48
|
+
}
|