@motion-proto/live-tokens 0.14.1 → 0.16.1
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 +78 -62
- package/dist-plugin/index.cjs +15 -4
- package/dist-plugin/index.js +15 -4
- package/package.json +1 -1
- package/src/editor/bootstrap.ts +55 -0
- package/src/editor/component-editor/SideNavigationEditor.svelte +20 -5
- package/src/editor/index.ts +4 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +165 -0
- package/src/system/components/SideNavigation.svelte +72 -78
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ export default defineConfig({
|
|
|
41
41
|
|
|
42
42
|
The `themeFileApi` plugin:
|
|
43
43
|
- Seeds `src/live-tokens/data/themes/` with a default theme on first dev-server start.
|
|
44
|
-
- Discovers components at `src/system/components/*.svelte` and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
|
|
44
|
+
- Discovers components at `src/components/*.svelte` (and `src/system/components/*.svelte` for back-compat) and seeds `src/live-tokens/data/component-configs/{comp}/default.json` from each component's `:global(:root)` block.
|
|
45
45
|
- Hosts the `/api/live-tokens/*` routes the editor uses to save and load themes + per-component configs.
|
|
46
46
|
- Auto-injects `__PROJECT_ROOT__` for the overlay's "Page Source" link and `__LIVE_TOKENS_API_BASE__` so the client uses whatever `apiBase` you configured.
|
|
47
47
|
|
|
@@ -73,66 +73,79 @@ Resolution order, per folder: explicit `themeFileApi(opts)` argument > matching
|
|
|
73
73
|
### Bootstrap in `main.ts`
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
|
|
77
|
-
import
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
initRouter,
|
|
82
|
-
initColumnsOverlay,
|
|
83
|
-
initEditorStore,
|
|
84
|
-
} from '@motion-proto/live-tokens';
|
|
76
|
+
// main.ts
|
|
77
|
+
import '@motion-proto/live-tokens/app/tokens.css';
|
|
78
|
+
import './live-tokens/data/tokens.generated.css';
|
|
79
|
+
import '@motion-proto/live-tokens/app/fonts.css';
|
|
80
|
+
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
85
81
|
import App from './App.svelte';
|
|
86
|
-
import { mount } from 'svelte';
|
|
87
82
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
mount(App, { target: document.getElementById('app')! });
|
|
99
|
-
}
|
|
83
|
+
bootLiveTokens(App, '#app');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`bootLiveTokens` orchestrates the editor's idempotent init hooks, fetches the
|
|
87
|
+
active theme in dev, optionally registers consumer-authored components, and
|
|
88
|
+
mounts the app. FontAwesome is side-effect-imported by the bootstrap (the dev
|
|
89
|
+
overlay always needs icons). The three token-CSS imports stay with the
|
|
90
|
+
consumer because order matters and `tokens.generated.css` is project-local.
|
|
91
|
+
|
|
92
|
+
Pass `components` to register consumer-authored editable components:
|
|
100
93
|
|
|
101
|
-
|
|
94
|
+
```ts
|
|
95
|
+
import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
|
|
96
|
+
|
|
97
|
+
bootLiveTokens(App, '#app', {
|
|
98
|
+
components: [{
|
|
99
|
+
id: 'mywidget',
|
|
100
|
+
label: 'My Widget',
|
|
101
|
+
icon: 'fas fa-magic',
|
|
102
|
+
sourceFile: 'src/components/MyWidget.svelte',
|
|
103
|
+
editorComponent: MyWidgetEditor,
|
|
104
|
+
schema: myWidgetTokens,
|
|
105
|
+
}],
|
|
106
|
+
});
|
|
102
107
|
```
|
|
103
108
|
|
|
104
|
-
### Mount
|
|
109
|
+
### Mount routes with `<LiveTokensRouter>`
|
|
105
110
|
|
|
106
111
|
```svelte
|
|
107
112
|
<!-- App.svelte -->
|
|
108
113
|
<script lang="ts">
|
|
109
|
-
import {
|
|
110
|
-
import Editor from '@motion-proto/live-tokens/editor';
|
|
111
|
-
import ComponentEditorPage from '@motion-proto/live-tokens/component-editor-page';
|
|
112
|
-
import { route } from './router';
|
|
114
|
+
import { LiveTokensRouter } from '@motion-proto/live-tokens';
|
|
113
115
|
</script>
|
|
114
116
|
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
{ path: '/components', label: 'Components', icon: 'fa-puzzle-piece' },
|
|
119
|
-
]}
|
|
120
|
-
pageSources={{
|
|
121
|
-
'/': 'src/app/Home.svelte',
|
|
122
|
-
}}
|
|
123
|
-
/>
|
|
124
|
-
<ColumnsOverlay />
|
|
125
|
-
|
|
126
|
-
{#if $route === '/editor'}
|
|
127
|
-
<Editor />
|
|
128
|
-
{:else if $route === '/components'}
|
|
129
|
-
<ComponentEditorPage />
|
|
130
|
-
{:else}
|
|
131
|
-
<!-- your routes -->
|
|
132
|
-
{/if}
|
|
117
|
+
<LiveTokensRouter pages={{
|
|
118
|
+
'/': { lazy: () => import('./Home.svelte'), label: 'Home', icon: 'fa-home', source: 'src/Home.svelte' },
|
|
119
|
+
}} />
|
|
133
120
|
```
|
|
134
121
|
|
|
135
|
-
`<
|
|
122
|
+
`<LiveTokensRouter>` owns the dev overlay (`<LiveEditorOverlay>` +
|
|
123
|
+
`<ColumnsOverlay>`), the editor routes (`/editor`, `/components`), the
|
|
124
|
+
in-app link-click interception, and the nav-rail/page-source plumbing the
|
|
125
|
+
overlay needs. Each entry in `pages` is one of your routes; entries with a
|
|
126
|
+
`label` appear in the overlay's nav rail. Pass pages as `lazy: () => import('./Page.svelte')`
|
|
127
|
+
so each page's stylesheet side-effects only evaluate when that route is
|
|
128
|
+
visited; pass `component: PageComponent` instead for an eagerly-imported
|
|
129
|
+
page. The editor routes are dispatched internally, so you don't have to
|
|
130
|
+
dynamic-import the library's editor pages yourself.
|
|
131
|
+
|
|
132
|
+
You can also relocate or disable a default editor route via the
|
|
133
|
+
`editorRoutes` prop: `<LiveTokensRouter pages={…} editorRoutes={{ editor: '/admin/editor', components: false }} />`.
|
|
134
|
+
Pass a string to move a route; pass `false` to remove the route entirely
|
|
135
|
+
(no dispatch and, for `components`, no auto-injected nav-rail entry).
|
|
136
|
+
|
|
137
|
+
The whole overlay surface is dev-only and tree-shakes out of production
|
|
138
|
+
builds — no `{#if import.meta.env.DEV}` guards needed.
|
|
139
|
+
|
|
140
|
+
### Lower-level API (when you need it)
|
|
141
|
+
|
|
142
|
+
`bootLiveTokens` and `<LiveTokensRouter>` are convenience wrappers. The
|
|
143
|
+
individual init functions (`initCssVarSync`, `initRouter`,
|
|
144
|
+
`initColumnsOverlay`, `initEditorStore`, `initializeTheme`),
|
|
145
|
+
`<LiveEditorOverlay>`, `<ColumnsOverlay>`, and the editor page exports
|
|
146
|
+
(`@motion-proto/live-tokens/editor`,
|
|
147
|
+
`@motion-proto/live-tokens/component-editor-page`) all stay exported. Use
|
|
148
|
+
them directly if you need a custom shell or non-standard route dispatch.
|
|
136
149
|
|
|
137
150
|
### Use components
|
|
138
151
|
|
|
@@ -199,7 +212,7 @@ export default defineConfig({
|
|
|
199
212
|
});
|
|
200
213
|
```
|
|
201
214
|
|
|
202
|
-
No `css: 'injected'` workaround, no `optimizeDeps` excludes — `vite build` works as-is. (You'll want the full `themeFileApi` plugin from the Quick install section above when you're ready to persist edits to disk.)
|
|
215
|
+
No `css: 'injected'` workaround, no `optimizeDeps` excludes — `vite build` works as-is. (You'll want the full `themeFileApi` plugin and `bootLiveTokens` / `<LiveTokensRouter>` from the Quick install section above when you're ready to persist edits to disk and ship a real app.)
|
|
203
216
|
|
|
204
217
|
## Greenfield? Use the starter
|
|
205
218
|
|
|
@@ -216,25 +229,28 @@ Open http://localhost:5173 and replace `src/app/Home.svelte` with your content.
|
|
|
216
229
|
|
|
217
230
|
## Consumer-authored components
|
|
218
231
|
|
|
219
|
-
The shipped components are first-party by default, but you can author your own and get the same real-time editing experience
|
|
232
|
+
The shipped components are first-party by default, but you can author your own and get the same real-time editing experience. Co-locate runtime and editor files in `src/components/` (or `src/system/components/`, both are scanned by default) and pass them to `bootLiveTokens`:
|
|
220
233
|
|
|
221
234
|
```ts
|
|
222
235
|
// src/main.ts
|
|
223
|
-
import {
|
|
224
|
-
import
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
236
|
+
import { bootLiveTokens } from '@motion-proto/live-tokens';
|
|
237
|
+
import App from './App.svelte';
|
|
238
|
+
import MyWidgetEditor, { allTokens as myWidgetTokens } from './components/MyWidgetEditor.svelte';
|
|
239
|
+
|
|
240
|
+
bootLiveTokens(App, '#app', {
|
|
241
|
+
components: [{
|
|
242
|
+
id: 'mywidget',
|
|
243
|
+
label: 'My Widget',
|
|
244
|
+
icon: 'fas fa-magic',
|
|
245
|
+
sourceFile: 'src/components/MyWidget.svelte',
|
|
246
|
+
editorComponent: MyWidgetEditor,
|
|
247
|
+
schema: myWidgetTokens,
|
|
248
|
+
}],
|
|
233
249
|
});
|
|
234
|
-
|
|
235
|
-
// then mount(App, ...)
|
|
236
250
|
```
|
|
237
251
|
|
|
252
|
+
(`bootLiveTokens` calls `registerComponent` internally for each entry, gated on `import.meta.env.DEV` so the registration tree-shakes out of production builds. Call `registerComponent` directly if you need finer control over timing.)
|
|
253
|
+
|
|
238
254
|
The component appears in the `/components` page under a **CUSTOM** group in the nav rail. Token rows, linked-block sharing, per-component config persistence, and reset-to-default work identically to the built-in set. All imports must come from `@motion-proto/live-tokens` or `@motion-proto/live-tokens/component-editor`; never deep-import from `src/`.
|
|
239
255
|
|
|
240
256
|
## Claude Code skills
|
package/dist-plugin/index.cjs
CHANGED
|
@@ -635,7 +635,7 @@ function themeFileApi(opts) {
|
|
|
635
635
|
const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? import_path3.default.resolve(opts.tokensGeneratedCssPath) : import_path3.default.join(dataDirs.dataDir, "tokens.generated.css");
|
|
636
636
|
const FONTS_CSS_PATH = opts.fontsCssPath ? import_path3.default.resolve(opts.fontsCssPath) : import_path3.default.join(import_path3.default.dirname(CSS_PATH), "fonts.css");
|
|
637
637
|
const API_BASE = opts.apiBase ?? "/api/live-tokens";
|
|
638
|
-
const
|
|
638
|
+
const consumerComponentDirs = opts.componentsSrcDir ? [import_path3.default.resolve(opts.componentsSrcDir)] : [import_path3.default.resolve("src/components"), import_path3.default.resolve("src/system/components")];
|
|
639
639
|
const packageComponentsDir = import_path3.default.resolve(
|
|
640
640
|
import_path3.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)),
|
|
641
641
|
"..",
|
|
@@ -643,8 +643,8 @@ function themeFileApi(opts) {
|
|
|
643
643
|
"system",
|
|
644
644
|
"components"
|
|
645
645
|
);
|
|
646
|
-
const COMPONENTS_SCAN_DIRS = [
|
|
647
|
-
if (packageComponentsDir
|
|
646
|
+
const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
|
|
647
|
+
if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && import_fs3.default.existsSync(packageComponentsDir)) {
|
|
648
648
|
COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
|
|
649
649
|
}
|
|
650
650
|
const LEGACY_PRESETS_DIR = import_path3.default.resolve("presets");
|
|
@@ -846,14 +846,24 @@ function themeFileApi(opts) {
|
|
|
846
846
|
function componentNameFromFile(filePath) {
|
|
847
847
|
return import_path3.default.basename(filePath, ".svelte").toLowerCase();
|
|
848
848
|
}
|
|
849
|
+
function isThemeAwareComponent(filePath) {
|
|
850
|
+
try {
|
|
851
|
+
const source = import_fs3.default.readFileSync(filePath, "utf-8");
|
|
852
|
+
return /:global\(:root\)\s*\{/.test(source);
|
|
853
|
+
} catch {
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
849
857
|
function listComponentSourcePaths() {
|
|
850
858
|
const byName = /* @__PURE__ */ new Map();
|
|
851
859
|
for (const dir of COMPONENTS_SCAN_DIRS) {
|
|
852
860
|
if (!import_fs3.default.existsSync(dir)) continue;
|
|
853
861
|
for (const f of import_fs3.default.readdirSync(dir)) {
|
|
854
862
|
if (!f.endsWith(".svelte")) continue;
|
|
863
|
+
const full = import_path3.default.join(dir, f);
|
|
864
|
+
if (!isThemeAwareComponent(full)) continue;
|
|
855
865
|
const name = componentNameFromFile(f);
|
|
856
|
-
if (!byName.has(name)) byName.set(name,
|
|
866
|
+
if (!byName.has(name)) byName.set(name, full);
|
|
857
867
|
}
|
|
858
868
|
}
|
|
859
869
|
return Array.from(byName.values());
|
|
@@ -1695,6 +1705,7 @@ function themeFileApi(opts) {
|
|
|
1695
1705
|
const normalized = import_path3.default.resolve(ctx.file);
|
|
1696
1706
|
if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
|
|
1697
1707
|
if (!normalized.endsWith(".svelte")) return;
|
|
1708
|
+
if (!isThemeAwareComponent(normalized)) return;
|
|
1698
1709
|
const comp = componentNameFromFile(normalized);
|
|
1699
1710
|
generateDefaultConfig(comp, normalized);
|
|
1700
1711
|
}
|
package/dist-plugin/index.js
CHANGED
|
@@ -598,7 +598,7 @@ function themeFileApi(opts) {
|
|
|
598
598
|
const GENERATED_CSS_PATH = opts.tokensGeneratedCssPath ? path3.resolve(opts.tokensGeneratedCssPath) : path3.join(dataDirs.dataDir, "tokens.generated.css");
|
|
599
599
|
const FONTS_CSS_PATH = opts.fontsCssPath ? path3.resolve(opts.fontsCssPath) : path3.join(path3.dirname(CSS_PATH), "fonts.css");
|
|
600
600
|
const API_BASE = opts.apiBase ?? "/api/live-tokens";
|
|
601
|
-
const
|
|
601
|
+
const consumerComponentDirs = opts.componentsSrcDir ? [path3.resolve(opts.componentsSrcDir)] : [path3.resolve("src/components"), path3.resolve("src/system/components")];
|
|
602
602
|
const packageComponentsDir = path3.resolve(
|
|
603
603
|
path3.dirname(fileURLToPath(import.meta.url)),
|
|
604
604
|
"..",
|
|
@@ -606,8 +606,8 @@ function themeFileApi(opts) {
|
|
|
606
606
|
"system",
|
|
607
607
|
"components"
|
|
608
608
|
);
|
|
609
|
-
const COMPONENTS_SCAN_DIRS = [
|
|
610
|
-
if (packageComponentsDir
|
|
609
|
+
const COMPONENTS_SCAN_DIRS = [...consumerComponentDirs];
|
|
610
|
+
if (!COMPONENTS_SCAN_DIRS.includes(packageComponentsDir) && fs3.existsSync(packageComponentsDir)) {
|
|
611
611
|
COMPONENTS_SCAN_DIRS.push(packageComponentsDir);
|
|
612
612
|
}
|
|
613
613
|
const LEGACY_PRESETS_DIR = path3.resolve("presets");
|
|
@@ -809,14 +809,24 @@ function themeFileApi(opts) {
|
|
|
809
809
|
function componentNameFromFile(filePath) {
|
|
810
810
|
return path3.basename(filePath, ".svelte").toLowerCase();
|
|
811
811
|
}
|
|
812
|
+
function isThemeAwareComponent(filePath) {
|
|
813
|
+
try {
|
|
814
|
+
const source = fs3.readFileSync(filePath, "utf-8");
|
|
815
|
+
return /:global\(:root\)\s*\{/.test(source);
|
|
816
|
+
} catch {
|
|
817
|
+
return false;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
812
820
|
function listComponentSourcePaths() {
|
|
813
821
|
const byName = /* @__PURE__ */ new Map();
|
|
814
822
|
for (const dir of COMPONENTS_SCAN_DIRS) {
|
|
815
823
|
if (!fs3.existsSync(dir)) continue;
|
|
816
824
|
for (const f of fs3.readdirSync(dir)) {
|
|
817
825
|
if (!f.endsWith(".svelte")) continue;
|
|
826
|
+
const full = path3.join(dir, f);
|
|
827
|
+
if (!isThemeAwareComponent(full)) continue;
|
|
818
828
|
const name = componentNameFromFile(f);
|
|
819
|
-
if (!byName.has(name)) byName.set(name,
|
|
829
|
+
if (!byName.has(name)) byName.set(name, full);
|
|
820
830
|
}
|
|
821
831
|
}
|
|
822
832
|
return Array.from(byName.values());
|
|
@@ -1658,6 +1668,7 @@ function themeFileApi(opts) {
|
|
|
1658
1668
|
const normalized = path3.resolve(ctx.file);
|
|
1659
1669
|
if (!COMPONENTS_SCAN_DIRS.some((d) => normalized.startsWith(d))) return;
|
|
1660
1670
|
if (!normalized.endsWith(".svelte")) return;
|
|
1671
|
+
if (!isThemeAwareComponent(normalized)) return;
|
|
1661
1672
|
const comp = componentNameFromFile(normalized);
|
|
1662
1673
|
generateDefaultConfig(comp, normalized);
|
|
1663
1674
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-call boot for a live-tokens consumer app.
|
|
3
|
+
*
|
|
4
|
+
* Bundles the five idempotent `init*` hooks that previously had to be
|
|
5
|
+
* orchestrated by every consumer (see README "Bootstrap in main.ts"), runs
|
|
6
|
+
* `initializeTheme` in dev, optionally registers custom components, and
|
|
7
|
+
* mounts the consumer's App at the target.
|
|
8
|
+
*
|
|
9
|
+
* CSS imports stay with the consumer — token CSS is order-sensitive
|
|
10
|
+
* (defaults → editor overrides → fonts) and the consumer's
|
|
11
|
+
* `tokens.generated.css` is per-project. FA icons are side-effect-imported
|
|
12
|
+
* here because the dev overlay always needs them and a consumer must not
|
|
13
|
+
* have to remember to import an icon font.
|
|
14
|
+
*/
|
|
15
|
+
import '@fortawesome/fontawesome-free/css/all.min.css';
|
|
16
|
+
|
|
17
|
+
import { mount, type Component } from 'svelte';
|
|
18
|
+
import * as cssVarSync from './core/cssVarSync';
|
|
19
|
+
import * as router from './core/routing/router';
|
|
20
|
+
import * as columnsOverlay from './overlay/columnsOverlay';
|
|
21
|
+
import * as editorStore from './core/store/editorStore';
|
|
22
|
+
import { initializeTheme } from './core/themes/themeInit';
|
|
23
|
+
import { registerComponent, type RegisterComponentEntry } from './component-editor/registry';
|
|
24
|
+
|
|
25
|
+
export interface BootLiveTokensOptions {
|
|
26
|
+
/** Consumer-authored components to register with the editor before mount. Dev-only. */
|
|
27
|
+
components?: RegisterComponentEntry[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function bootLiveTokens(
|
|
31
|
+
App: Component<any, any, any>,
|
|
32
|
+
target: string | Element,
|
|
33
|
+
opts: BootLiveTokensOptions = {},
|
|
34
|
+
): Promise<ReturnType<typeof mount>> {
|
|
35
|
+
cssVarSync.init();
|
|
36
|
+
router.init();
|
|
37
|
+
columnsOverlay.init();
|
|
38
|
+
editorStore.init();
|
|
39
|
+
|
|
40
|
+
if (import.meta.env.DEV) {
|
|
41
|
+
if (opts.components) {
|
|
42
|
+
for (const entry of opts.components) {
|
|
43
|
+
registerComponent(entry);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await initializeTheme();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const targetEl =
|
|
50
|
+
typeof target === 'string' ? document.querySelector(target) : target;
|
|
51
|
+
if (!targetEl) {
|
|
52
|
+
throw new Error(`bootLiveTokens: target ${JSON.stringify(target)} not found`);
|
|
53
|
+
}
|
|
54
|
+
return mount(App, { target: targetEl });
|
|
55
|
+
}
|
|
@@ -39,16 +39,22 @@
|
|
|
39
39
|
];
|
|
40
40
|
|
|
41
41
|
// --- Title --------------------------------------------------------------
|
|
42
|
+
// Title is a flex card containing the label box + toggle box. Per-state
|
|
43
|
+
// tokens drive the card chrome (surface, border, padding); stateless gap
|
|
44
|
+
// and radius live in `titleLayoutTokens` since they don't vary across
|
|
45
|
+
// default/hover/active.
|
|
42
46
|
function titleStateTokens(s: StatefulState): Token[] {
|
|
43
47
|
return [
|
|
44
48
|
{ label: 'surface color', groupKey: 'title-surface', variable: `--sidenavigation-title-${s}-surface` },
|
|
45
|
-
{ label: '
|
|
46
|
-
{ label: '
|
|
49
|
+
{ label: 'border color', groupKey: 'title-border', variable: `--sidenavigation-title-${s}-border` },
|
|
50
|
+
{ label: 'border width', canBeLinked: true, groupKey: 'title-border-width', variable: `--sidenavigation-title-${s}-border-width` },
|
|
47
51
|
{ label: 'padding', canBeLinked: true, groupKey: 'title-padding', variable: `--sidenavigation-title-${s}-padding` },
|
|
48
|
-
{ label: 'indicator color', groupKey: 'title-accent', variable: `--sidenavigation-title-${s}-accent` },
|
|
49
|
-
{ label: 'indicator width', canBeLinked: true, groupKey: 'title-accent-width', variable: `--sidenavigation-title-${s}-accent-width` },
|
|
50
52
|
];
|
|
51
53
|
}
|
|
54
|
+
const titleLayoutTokens: Token[] = [
|
|
55
|
+
{ label: 'gap', canBeLinked: true, groupKey: 'title-gap', variable: '--sidenavigation-title-gap' },
|
|
56
|
+
{ label: 'corner radius', canBeLinked: true, groupKey: 'title-radius', variable: '--sidenavigation-title-radius' },
|
|
57
|
+
];
|
|
52
58
|
function titleStateTypeGroups(s: StatefulState): TypeGroupConfig[] {
|
|
53
59
|
return [{
|
|
54
60
|
legend: 'title label',
|
|
@@ -59,6 +65,14 @@
|
|
|
59
65
|
lineHeightVariable: `--sidenavigation-title-${s}-label-line-height`,
|
|
60
66
|
}];
|
|
61
67
|
}
|
|
68
|
+
// Title label — structural inner box (stateless, like Panel). Sits inside
|
|
69
|
+
// the title bar so the header reads as: outer bar → [label box] [toggle box].
|
|
70
|
+
const titleLabelTokens: Token[] = [
|
|
71
|
+
{ label: 'surface color', groupKey: 'title-label-surface', variable: '--sidenavigation-title-label-surface' },
|
|
72
|
+
{ label: 'corner radius', canBeLinked: true, groupKey: 'title-label-radius', variable: '--sidenavigation-title-label-radius' },
|
|
73
|
+
{ label: 'padding', canBeLinked: true, groupKey: 'title-label-padding', variable: '--sidenavigation-title-label-padding' },
|
|
74
|
+
];
|
|
75
|
+
|
|
62
76
|
const titleTypographyTokens: Token[] = STATEFUL_STATES.flatMap((s) => [
|
|
63
77
|
{ label: 'font family', canBeLinked: true, groupKey: 'title-label-font-family', variable: `--sidenavigation-title-${s}-label-font-family` },
|
|
64
78
|
{ label: 'font size', canBeLinked: true, groupKey: 'title-label-font-size', variable: `--sidenavigation-title-${s}-label-font-size` },
|
|
@@ -168,6 +182,8 @@
|
|
|
168
182
|
const states: Record<string, Token[]> = {
|
|
169
183
|
'Panel': panelTokens,
|
|
170
184
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Title / ${STATE_LABELS[s]}`, titleStateTokens(s)])),
|
|
185
|
+
'Title Layout': titleLayoutTokens,
|
|
186
|
+
'Title Label': titleLabelTokens,
|
|
171
187
|
...Object.fromEntries(TOGGLE_STATES.map((s) => [`Toggle / ${STATE_LABELS[s]}`, toggleStateTokens(s)])),
|
|
172
188
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Section / ${STATE_LABELS[s]}`, sectionStateTokens(s)])),
|
|
173
189
|
...Object.fromEntries(STATEFUL_STATES.map((s) => [`Item / ${STATE_LABELS[s]}`, itemStateTokens(s)])),
|
|
@@ -196,7 +212,6 @@
|
|
|
196
212
|
...STATEFUL_STATES.flatMap((s): Array<[string, string]> => [
|
|
197
213
|
[`--sidenavigation-title-${s}-border-width`, `title ${s}`],
|
|
198
214
|
[`--sidenavigation-title-${s}-padding`, `title ${s}`],
|
|
199
|
-
[`--sidenavigation-title-${s}-accent-width`, `title ${s}`],
|
|
200
215
|
[`--sidenavigation-title-${s}-label-font-family`, `title ${s}`],
|
|
201
216
|
[`--sidenavigation-title-${s}-label-font-size`, `title ${s}`],
|
|
202
217
|
[`--sidenavigation-title-${s}-label-font-weight`, `title ${s}`],
|
package/src/editor/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { default as LiveEditorOverlay } from './overlay/LiveEditorOverlay.svelte';
|
|
2
2
|
export type { NavLink } from './core/routing/navLinkTypes';
|
|
3
3
|
export { default as ColumnsOverlay } from './overlay/ColumnsOverlay.svelte';
|
|
4
|
+
export { default as LiveTokensRouter } from './overlay/LiveTokensRouter.svelte';
|
|
5
|
+
export type { RouteEntry, EditorRouteOverrides } from './overlay/LiveTokensRouter.svelte';
|
|
6
|
+
export { bootLiveTokens } from './bootstrap';
|
|
7
|
+
export type { BootLiveTokensOptions } from './bootstrap';
|
|
4
8
|
|
|
5
9
|
export { columnsVisible, toggleColumns, init as initColumnsOverlay } from './overlay/columnsOverlay';
|
|
6
10
|
export { configureEditor, storageKey } from './core/store/editorConfig';
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One entry per consumer route. `label` + `icon` make it appear in the
|
|
6
|
+
* overlay's nav rail; `source` powers the "Page Source" button. Omit
|
|
7
|
+
* `label` to keep the route reachable by URL but absent from the rail
|
|
8
|
+
* (matches the existing playground/unlisted route pattern).
|
|
9
|
+
*
|
|
10
|
+
* Provide either `component` (eager) or `lazy` (code-split). Use `lazy`
|
|
11
|
+
* for any page that side-effect-imports a stylesheet at the top of its
|
|
12
|
+
* module, so those imports only evaluate when the route is visited and
|
|
13
|
+
* don't leak into unrelated routes (most importantly the editor pages).
|
|
14
|
+
*/
|
|
15
|
+
export interface RouteEntry {
|
|
16
|
+
component?: Component<any, any, any>;
|
|
17
|
+
lazy?: () => Promise<{ default: Component<any, any, any> }>;
|
|
18
|
+
label?: string;
|
|
19
|
+
icon?: string;
|
|
20
|
+
source?: string;
|
|
21
|
+
/** Hide the overlay's "Page Source" button on this route. */
|
|
22
|
+
hidePageSource?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Override the default editor routes (`/editor`, `/components`). Pass a
|
|
27
|
+
* string to relocate the route; pass `false` to disable it entirely (no
|
|
28
|
+
* dispatch and, for `components`, no auto-injected nav-rail entry).
|
|
29
|
+
*/
|
|
30
|
+
export interface EditorRouteOverrides {
|
|
31
|
+
editor?: string | false;
|
|
32
|
+
components?: string | false;
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<script lang="ts">
|
|
37
|
+
import LiveEditorOverlay from './LiveEditorOverlay.svelte';
|
|
38
|
+
import ColumnsOverlay from './ColumnsOverlay.svelte';
|
|
39
|
+
import { route, navigate } from '../core/routing/router';
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
pages: Record<string, RouteEntry>;
|
|
43
|
+
editorRoutes?: EditorRouteOverrides;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let { pages, editorRoutes = {} }: Props = $props();
|
|
47
|
+
|
|
48
|
+
let editorEnabled = $derived(editorRoutes.editor !== false);
|
|
49
|
+
let componentsEnabled = $derived(editorRoutes.components !== false);
|
|
50
|
+
let editorPath = $derived(typeof editorRoutes.editor === 'string' ? editorRoutes.editor : '/editor');
|
|
51
|
+
let componentsPath = $derived(typeof editorRoutes.components === 'string' ? editorRoutes.components : '/components');
|
|
52
|
+
|
|
53
|
+
const isDev = import.meta.env.DEV;
|
|
54
|
+
let isEditor = $derived(isDev && editorEnabled && $route === editorPath);
|
|
55
|
+
let isComponentEditor = $derived(isDev && componentsEnabled && $route === componentsPath);
|
|
56
|
+
|
|
57
|
+
// Pages with a label show up in the nav rail, in declaration order. In dev,
|
|
58
|
+
// the components-editor route is auto-appended so it's reachable from every
|
|
59
|
+
// page (the convention every existing live-tokens consumer has settled on).
|
|
60
|
+
let navLinks = $derived([
|
|
61
|
+
...Object.entries(pages)
|
|
62
|
+
.filter(([, e]) => !!e.label)
|
|
63
|
+
.map(([path, e]) => ({ path, label: e.label!, icon: e.icon ?? '' })),
|
|
64
|
+
...(isDev && componentsEnabled
|
|
65
|
+
? [{ path: componentsPath, label: 'Components', icon: 'fa-puzzle-piece' }]
|
|
66
|
+
: []),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
let pageSources = $derived(
|
|
70
|
+
Object.fromEntries(
|
|
71
|
+
Object.entries(pages)
|
|
72
|
+
.filter(([, e]) => !!e.source)
|
|
73
|
+
.map(([path, e]) => [path, e.source!]),
|
|
74
|
+
),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
// Components-editor route always hides the page-source button (matches the
|
|
78
|
+
// convention from the original consumer pattern).
|
|
79
|
+
let hidePageSourceOn = $derived([
|
|
80
|
+
...Object.entries(pages)
|
|
81
|
+
.filter(([, e]) => e.hidePageSource)
|
|
82
|
+
.map(([path]) => path),
|
|
83
|
+
...(componentsEnabled ? [componentsPath] : []),
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
// Dispatch the current route. Editor pages are dynamically imported so they
|
|
87
|
+
// don't ship in non-editor route bundles. Consumer pages dispatch via
|
|
88
|
+
// `entry.lazy()` when provided (so each page's CSS side-effect imports stay
|
|
89
|
+
// out of other routes), or via the eagerly-imported `entry.component`.
|
|
90
|
+
let pagePromise = $derived.by(() => {
|
|
91
|
+
if (isEditor) return import('../pages/Editor.svelte');
|
|
92
|
+
if (isComponentEditor) return import('../pages/ComponentEditorPage.svelte');
|
|
93
|
+
const entry = pages[$route] ?? pages['/'];
|
|
94
|
+
if (!entry) return Promise.resolve({ default: null as unknown as Component<any, any, any> });
|
|
95
|
+
if (entry.lazy) return entry.lazy();
|
|
96
|
+
if (entry.component) return Promise.resolve({ default: entry.component });
|
|
97
|
+
return Promise.resolve({ default: null as unknown as Component<any, any, any> });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// In-app link interception: turn left-clicks on internal `/...` anchors into
|
|
101
|
+
// navigate() calls so router state updates without a full reload. Modifier
|
|
102
|
+
// keys (cmd/ctrl/shift/alt) pass through to the browser's default handling.
|
|
103
|
+
function handleClick(e: MouseEvent) {
|
|
104
|
+
const anchor = (e.target as HTMLElement).closest('a[href]');
|
|
105
|
+
if (!anchor) return;
|
|
106
|
+
const href = anchor.getAttribute('href');
|
|
107
|
+
if (!href || !href.startsWith('/')) return;
|
|
108
|
+
if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
|
|
109
|
+
e.preventDefault();
|
|
110
|
+
navigate(href);
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions, a11y_no_static_element_interactions -->
|
|
115
|
+
<div
|
|
116
|
+
class="lt-app"
|
|
117
|
+
class:is-editor={isEditor}
|
|
118
|
+
class:is-component-editor={isComponentEditor}
|
|
119
|
+
onclick={handleClick}
|
|
120
|
+
>
|
|
121
|
+
<LiveEditorOverlay {navLinks} {pageSources} {hidePageSourceOn} {editorPath} />
|
|
122
|
+
<ColumnsOverlay />
|
|
123
|
+
|
|
124
|
+
{#await pagePromise then m}
|
|
125
|
+
{@const PageComponent = m.default}
|
|
126
|
+
{#if PageComponent}
|
|
127
|
+
<PageComponent />
|
|
128
|
+
{/if}
|
|
129
|
+
{/await}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<style>
|
|
133
|
+
:global(html) {
|
|
134
|
+
scrollbar-gutter: stable;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
:global(body) {
|
|
138
|
+
background: var(--page-bg);
|
|
139
|
+
background-attachment: var(--page-bg-attachment, fixed);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.lt-app {
|
|
143
|
+
width: 100%;
|
|
144
|
+
min-height: 100vh;
|
|
145
|
+
color: var(--text-primary);
|
|
146
|
+
padding-bottom: 12rem;
|
|
147
|
+
/* Set by LiveEditorOverlay when docked-open. Extends layout past the
|
|
148
|
+
fixed panel so the viewport can scroll to reveal hidden content. */
|
|
149
|
+
padding-right: var(--lt-overlay-scroll-pad, 0px);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.lt-app.is-editor {
|
|
153
|
+
padding-bottom: 0;
|
|
154
|
+
background: black;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.lt-app.is-component-editor {
|
|
158
|
+
min-height: 0;
|
|
159
|
+
height: 100vh;
|
|
160
|
+
padding-bottom: 0;
|
|
161
|
+
padding-right: 0;
|
|
162
|
+
background: black;
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
}
|
|
165
|
+
</style>
|
|
@@ -139,28 +139,28 @@
|
|
|
139
139
|
class:force-footer-hover={forceHoverPart === 'footer'}
|
|
140
140
|
class:force-footer-active={forceActivePart === 'footer'}
|
|
141
141
|
>
|
|
142
|
-
<!--
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
onclick={fireToggle}
|
|
150
|
-
aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
151
|
-
aria-expanded={open}
|
|
152
|
-
>
|
|
153
|
-
<i class="fa-solid fa-angles-right" aria-hidden="true"></i>
|
|
154
|
-
</button>
|
|
155
|
-
|
|
156
|
-
{#if open}
|
|
157
|
-
<!-- Open layout. Title bar reserves space for the persistent toggle on
|
|
158
|
-
the right; the menu is locked at the open-width so it can't reflow
|
|
159
|
-
while the rail expands around it. -->
|
|
160
|
-
<header class="sn-title" class:active={titleActive}>
|
|
142
|
+
<!-- Header is always rendered so the toggle (a child here) survives the
|
|
143
|
+
collapsed state. The label is the only conditional child — when the
|
|
144
|
+
rail is closed, the header reduces to just the toggle, centred via the
|
|
145
|
+
toggle's own `left` calc. Header stays locked to open-width regardless;
|
|
146
|
+
the aside's overflow clips it during the close animation. -->
|
|
147
|
+
<header class="sn-title" class:active={titleActive}>
|
|
148
|
+
{#if open}
|
|
161
149
|
<a href={titleHref} class="sn-title-label">{titleLabel}</a>
|
|
162
|
-
|
|
150
|
+
{/if}
|
|
151
|
+
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
class="sn-toggle"
|
|
155
|
+
onclick={fireToggle}
|
|
156
|
+
aria-label={open ? 'Collapse sidebar' : 'Expand sidebar'}
|
|
157
|
+
aria-expanded={open}
|
|
158
|
+
>
|
|
159
|
+
<i class="fa-solid fa-angles-right" aria-hidden="true"></i>
|
|
160
|
+
</button>
|
|
161
|
+
</header>
|
|
163
162
|
|
|
163
|
+
{#if open}
|
|
164
164
|
<div class="sn-menu">
|
|
165
165
|
{#each sections as section (section.path)}
|
|
166
166
|
<div class="sn-section">
|
|
@@ -228,6 +228,22 @@
|
|
|
228
228
|
--sidenavigation-close-duration: var(--duration-150);
|
|
229
229
|
--sidenavigation-close-easing: var(--ease-out-quart);
|
|
230
230
|
|
|
231
|
+
/* Title — layout (stateless). The header is a flex container that hosts
|
|
232
|
+
the label box and the toggle box side-by-side; gap and radius drive
|
|
233
|
+
the card-like outer shell. */
|
|
234
|
+
--sidenavigation-title-gap: var(--space-8);
|
|
235
|
+
--sidenavigation-title-radius: var(--radius-md);
|
|
236
|
+
|
|
237
|
+
/* Title label — structural inner box (stateless). Renders as a row inside
|
|
238
|
+
the title bar so the header reads as: outer card → [label box] [toggle
|
|
239
|
+
box]. align-items: stretch on the header equalises its height with the
|
|
240
|
+
toggle's. */
|
|
241
|
+
--sidenavigation-title-label-surface: var(--color-transparent);
|
|
242
|
+
--sidenavigation-title-label-border: var(--border-canvas-faint);
|
|
243
|
+
--sidenavigation-title-label-border-width: var(--border-width-1);
|
|
244
|
+
--sidenavigation-title-label-radius: var(--radius-md);
|
|
245
|
+
--sidenavigation-title-label-padding: var(--space-6);
|
|
246
|
+
|
|
231
247
|
/* Title — default */
|
|
232
248
|
--sidenavigation-title-default-surface: var(--color-transparent);
|
|
233
249
|
--sidenavigation-title-default-border: var(--border-canvas-faint);
|
|
@@ -242,7 +258,7 @@
|
|
|
242
258
|
--sidenavigation-title-default-label-line-height: var(--line-height-sm);
|
|
243
259
|
|
|
244
260
|
/* Title — hover */
|
|
245
|
-
--sidenavigation-title-hover-surface: var(--
|
|
261
|
+
--sidenavigation-title-hover-surface: var(--color-transparent);
|
|
246
262
|
--sidenavigation-title-hover-border: var(--border-canvas-faint);
|
|
247
263
|
--sidenavigation-title-hover-border-width: var(--border-width-1);
|
|
248
264
|
--sidenavigation-title-hover-padding: var(--space-12);
|
|
@@ -428,34 +444,27 @@
|
|
|
428
444
|
--_border: var(--sidenavigation-title-default-border);
|
|
429
445
|
--_border-width: var(--sidenavigation-title-default-border-width);
|
|
430
446
|
--_padding: var(--sidenavigation-title-default-padding);
|
|
431
|
-
--_indicator: var(--sidenavigation-title-default-accent);
|
|
432
|
-
--_indicator-width: var(--sidenavigation-title-default-accent-width);
|
|
433
447
|
--_label: var(--sidenavigation-title-default-label);
|
|
434
448
|
--_label-family: var(--sidenavigation-title-default-label-font-family);
|
|
435
449
|
--_label-size: var(--sidenavigation-title-default-label-font-size);
|
|
436
450
|
--_label-weight: var(--sidenavigation-title-default-label-font-weight);
|
|
437
451
|
--_label-line-height: var(--sidenavigation-title-default-label-line-height);
|
|
438
452
|
|
|
439
|
-
/*
|
|
440
|
-
|
|
453
|
+
/* The header is a pure flex row: card-like outer with the label box and
|
|
454
|
+
toggle box as siblings. No absolute positioning — the label fills with
|
|
455
|
+
flex:1, the toggle is a fixed-size sibling. justify-content: center
|
|
456
|
+
takes over when the label is removed in the collapsed state and the
|
|
457
|
+
toggle becomes the only child. */
|
|
441
458
|
flex: 0 0 auto;
|
|
442
|
-
/* Lock to the open-width so the title doesn't reflow during the rail
|
|
443
|
-
expansion (3rem → 16rem on open). border-box keeps the outer width
|
|
444
|
-
equal to the token value (otherwise padding + border would push the
|
|
445
|
-
toggle past the rail's right edge). */
|
|
446
459
|
box-sizing: border-box;
|
|
447
|
-
width: var(--sidenavigation-panel-open-width);
|
|
448
460
|
display: flex;
|
|
449
|
-
align-items:
|
|
450
|
-
|
|
461
|
+
align-items: stretch;
|
|
462
|
+
justify-content: center;
|
|
463
|
+
gap: var(--sidenavigation-title-gap);
|
|
451
464
|
background: var(--_surface);
|
|
452
|
-
border-
|
|
453
|
-
border-left: var(--_indicator-width) solid var(--_indicator);
|
|
465
|
+
border-radius: var(--sidenavigation-title-radius);
|
|
454
466
|
@include themed-padding(--_padding);
|
|
455
|
-
|
|
456
|
-
header (toggle width ~36px + 8px breathing room). */
|
|
457
|
-
padding-right: calc(var(--space-12) + 44px);
|
|
458
|
-
transition: background var(--duration-150), border-color var(--duration-150);
|
|
467
|
+
transition: background var(--duration-150);
|
|
459
468
|
}
|
|
460
469
|
|
|
461
470
|
.sn-title:hover:not(.active),
|
|
@@ -464,8 +473,6 @@
|
|
|
464
473
|
--_border: var(--sidenavigation-title-hover-border);
|
|
465
474
|
--_border-width: var(--sidenavigation-title-hover-border-width);
|
|
466
475
|
--_padding: var(--sidenavigation-title-hover-padding);
|
|
467
|
-
--_indicator: var(--sidenavigation-title-hover-accent);
|
|
468
|
-
--_indicator-width: var(--sidenavigation-title-hover-accent-width);
|
|
469
476
|
--_label: var(--sidenavigation-title-hover-label);
|
|
470
477
|
--_label-family: var(--sidenavigation-title-hover-label-font-family);
|
|
471
478
|
--_label-size: var(--sidenavigation-title-hover-label-font-size);
|
|
@@ -478,8 +485,6 @@
|
|
|
478
485
|
--_border: var(--sidenavigation-title-active-border);
|
|
479
486
|
--_border-width: var(--sidenavigation-title-active-border-width);
|
|
480
487
|
--_padding: var(--sidenavigation-title-active-padding);
|
|
481
|
-
--_indicator: var(--sidenavigation-title-active-accent);
|
|
482
|
-
--_indicator-width: var(--sidenavigation-title-active-accent-width);
|
|
483
488
|
--_label: var(--sidenavigation-title-active-label);
|
|
484
489
|
--_label-family: var(--sidenavigation-title-active-label-font-family);
|
|
485
490
|
--_label-size: var(--sidenavigation-title-active-label-font-size);
|
|
@@ -487,7 +492,18 @@
|
|
|
487
492
|
--_label-line-height: var(--sidenavigation-title-active-label-line-height);
|
|
488
493
|
}
|
|
489
494
|
|
|
495
|
+
/* In the collapsed state the header narrows with the rail. Drop horizontal
|
|
496
|
+
padding so the toggle (single remaining flex child) still fits in the
|
|
497
|
+
closed-width aside — open-state padding alone would push it out. */
|
|
498
|
+
.sidenavigation.collapsed .sn-title {
|
|
499
|
+
padding-left: 0;
|
|
500
|
+
padding-right: 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
490
503
|
.sn-title-label {
|
|
504
|
+
background: var(--sidenavigation-title-label-surface);
|
|
505
|
+
border-radius: var(--sidenavigation-title-label-radius);
|
|
506
|
+
@include themed-padding(--sidenavigation-title-label-padding);
|
|
491
507
|
color: var(--_label);
|
|
492
508
|
font-family: var(--_label-family);
|
|
493
509
|
font-size: var(--_label-size);
|
|
@@ -495,6 +511,8 @@
|
|
|
495
511
|
line-height: var(--_label-line-height);
|
|
496
512
|
text-decoration: none;
|
|
497
513
|
flex: 1 1 auto;
|
|
514
|
+
display: flex;
|
|
515
|
+
align-items: center;
|
|
498
516
|
min-width: 0;
|
|
499
517
|
white-space: nowrap;
|
|
500
518
|
overflow: hidden;
|
|
@@ -510,62 +528,38 @@
|
|
|
510
528
|
--_icon: var(--sidenavigation-toggle-default-icon);
|
|
511
529
|
--_icon-size: var(--sidenavigation-toggle-default-icon-size);
|
|
512
530
|
|
|
513
|
-
/*
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
position: absolute;
|
|
518
|
-
/* Toggle's outer width is derived from its constituent tokens — icon
|
|
519
|
-
+ padding (both sides) + border (both sides). Stays accurate when a
|
|
520
|
-
consumer customizes any of those without re-hardcoding here. */
|
|
521
|
-
--_toggle-width: calc(
|
|
522
|
-
var(--sidenavigation-toggle-default-icon-size)
|
|
523
|
-
+ 2 * var(--sidenavigation-toggle-default-padding)
|
|
524
|
-
+ 2 * var(--sidenavigation-toggle-default-border-width)
|
|
525
|
-
);
|
|
526
|
-
top: var(--space-12);
|
|
527
|
-
left: calc(var(--sidenavigation-panel-open-width) - var(--_toggle-width) - var(--space-8));
|
|
528
|
-
z-index: 1;
|
|
529
|
-
transition:
|
|
530
|
-
left var(--sidenavigation-open-duration) var(--sidenavigation-open-easing),
|
|
531
|
-
background var(--duration-150),
|
|
532
|
-
border-color var(--duration-150),
|
|
533
|
-
color var(--duration-150);
|
|
534
|
-
|
|
531
|
+
/* Regular flex child of .sn-title — no absolute positioning. The header
|
|
532
|
+
row's justify-content+flex:1-on-label combo puts the toggle at the
|
|
533
|
+
right edge when the label is present and centres it when the label
|
|
534
|
+
is removed in the collapsed state. */
|
|
535
535
|
display: inline-flex;
|
|
536
536
|
align-items: center;
|
|
537
537
|
justify-content: center;
|
|
538
|
-
flex
|
|
538
|
+
flex: 0 0 auto;
|
|
539
|
+
box-sizing: border-box;
|
|
539
540
|
background: var(--_surface);
|
|
540
541
|
border: var(--_border-width) solid var(--_border);
|
|
541
542
|
border-radius: var(--_radius);
|
|
542
543
|
color: var(--_icon);
|
|
543
544
|
@include themed-padding(--_padding);
|
|
544
545
|
cursor: pointer;
|
|
546
|
+
transition:
|
|
547
|
+
background var(--duration-150),
|
|
548
|
+
border-color var(--duration-150),
|
|
549
|
+
color var(--duration-150);
|
|
545
550
|
}
|
|
546
551
|
|
|
547
552
|
.sn-toggle i {
|
|
548
553
|
font-size: var(--_icon-size);
|
|
549
554
|
line-height: 1;
|
|
550
555
|
/* Icon points right by default (expand). Open state flips it to point
|
|
551
|
-
left (collapse). Rotation tweens with the
|
|
552
|
-
morphs smoothly between the two states. */
|
|
556
|
+
left (collapse). Rotation tweens with the rail's width transition so
|
|
557
|
+
the affordance morphs smoothly between the two states. */
|
|
553
558
|
transition: transform var(--sidenavigation-open-duration) var(--sidenavigation-open-easing);
|
|
554
559
|
}
|
|
555
560
|
.sidenavigation:not(.collapsed) .sn-toggle i {
|
|
556
561
|
transform: rotate(180deg);
|
|
557
562
|
}
|
|
558
|
-
|
|
559
|
-
/* Collapsed: centre the toggle in the rail, and swap to close-* timing
|
|
560
|
-
tokens so the leftward slide matches the rail's shrink. */
|
|
561
|
-
.sidenavigation.collapsed .sn-toggle {
|
|
562
|
-
left: calc((var(--sidenavigation-panel-closed-width) - var(--_toggle-width)) / 2);
|
|
563
|
-
transition:
|
|
564
|
-
left var(--sidenavigation-close-duration) var(--sidenavigation-close-easing),
|
|
565
|
-
background var(--duration-150),
|
|
566
|
-
border-color var(--duration-150),
|
|
567
|
-
color var(--duration-150);
|
|
568
|
-
}
|
|
569
563
|
.sidenavigation.collapsed .sn-toggle i {
|
|
570
564
|
transition: transform var(--sidenavigation-close-duration) var(--sidenavigation-close-easing);
|
|
571
565
|
}
|