@fuzdev/fuz_app 0.122.0 → 0.123.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/dist/ui/CLAUDE.md
CHANGED
|
@@ -221,7 +221,21 @@ pattern rather than reintroducing prop-drilling.
|
|
|
221
221
|
to keep narrow mode at `0px`. To change only the wide open offset, target
|
|
222
222
|
`.app-shell.wide-open > .app-shell-content`, also inside that negation.
|
|
223
223
|
- `ColumnLayout.svelte` — fixed `aside` column + fluid `children`
|
|
224
|
-
column; `column_width = '280px'`.
|
|
224
|
+
column; `column_width = '280px'`, `aside_label = 'menu'`. Below 64rem of
|
|
225
|
+
its own width (an `inline-size` container query named `column_layout`;
|
|
226
|
+
64rem under fuz_css's 62.5% root is fuz_css's `sm` breakpoint, 640px at
|
|
227
|
+
the default font size) the columns stack and the aside collapses behind a
|
|
228
|
+
sticky `type="button"` toggle (`menuitem plain` with a `chevron`, text
|
|
229
|
+
`aside_label`, `aria-expanded`/`aria-controls`); opened, the aside sits in flow above the content (scrolled into view under the toggle when scroll anchoring would leave it above the view), and `afterNavigate` closes it. With a bounded height, the stacked layout
|
|
230
|
+
scrolls as one column and the toggle sticks to its top; under a parent
|
|
231
|
+
that grows with its content, the page scrolls and the toggle scrolls away
|
|
232
|
+
with it. Above the threshold the toggle is `display: none`. The switch is
|
|
233
|
+
CSS alone, so the server render shows the right form at any width and
|
|
234
|
+
hydration changes nothing. The
|
|
235
|
+
`<aside>` takes `aria-label={aside_label}`; consumers wrap links in their
|
|
236
|
+
own `<nav>`. Hooks: `--column_width`, `--column_toggle_bg` (default
|
|
237
|
+
`--shade_00`). The container can't size from its content, so a
|
|
238
|
+
shrink-to-fit placement collapses it to zero width.
|
|
225
239
|
- `MenuLink.svelte` — SvelteKit `<a>` with `selected` derived from
|
|
226
240
|
`page.url.pathname` (fires for exact match + descendant pages).
|
|
227
241
|
Exact matches additionally get `aria-current="page"`. Takes `path`
|
|
@@ -1,18 +1,52 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
/**
|
|
3
|
-
* Two-column layout
|
|
3
|
+
* Two-column layout - fixed-width `aside` on the left, fluid `children`
|
|
4
4
|
* column on the right. Both columns scroll independently.
|
|
5
5
|
*
|
|
6
|
+
* Below a threshold of the layout's own width - a container query, not a
|
|
7
|
+
* media query, so a nested layout decides for itself - the columns stack:
|
|
8
|
+
* the aside collapses behind a toggle button labelled `aside_label`, and
|
|
9
|
+
* when opened it sits in flow above the content, pushing it down, until
|
|
10
|
+
* the toggle closes it or a navigation does. Opening scrolls the aside
|
|
11
|
+
* into view, under the toggle, when it would land above the view.
|
|
12
|
+
*
|
|
13
|
+
* With a bounded height, the stacked layout scrolls as one column and the
|
|
14
|
+
* toggle sticks to its top; under a parent that grows with its content,
|
|
15
|
+
* the page scrolls and the toggle scrolls away with it.
|
|
16
|
+
*
|
|
17
|
+
* The threshold matches fuz_css's `sm` breakpoint. Above it the toggle is
|
|
18
|
+
* `display: none`. The switch is CSS alone, so the server render shows the
|
|
19
|
+
* right form at any width and hydration changes nothing.
|
|
20
|
+
*
|
|
21
|
+
* The root is an `inline-size` query container, so its width can't come
|
|
22
|
+
* from its content: a layout placed where it would shrink to fit (an
|
|
23
|
+
* inline-block, an absolutely positioned or floated box without a width,
|
|
24
|
+
* a flex row item that doesn't grow) collapses to zero width. Give it a
|
|
25
|
+
* width from its context, as a block or a stretched or growing flex item.
|
|
26
|
+
*
|
|
27
|
+
* Requires the SvelteKit runtime - it closes the aside from
|
|
28
|
+
* `afterNavigate`. Without JS the stacked aside can't be opened, since the
|
|
29
|
+
* toggle is client state; above the threshold it's always shown.
|
|
30
|
+
*
|
|
31
|
+
* Renders a generic `<aside>` named by `aside_label`; wrap navigation
|
|
32
|
+
* links in a `<nav>` inside the `aside` snippet.
|
|
33
|
+
*
|
|
34
|
+
* Styling hooks: `--column_width` (set from `column_width`) and
|
|
35
|
+
* `--column_toggle_bg`, the stacked toggle bar's opaque background so
|
|
36
|
+
* content scrolls under it unseen (default `--shade_00`, the page ground).
|
|
37
|
+
*
|
|
6
38
|
* @module
|
|
7
39
|
*/
|
|
8
40
|
|
|
9
|
-
import type
|
|
41
|
+
import { tick, type Snippet } from 'svelte';
|
|
10
42
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
43
|
+
import { afterNavigate } from '$app/navigation';
|
|
11
44
|
|
|
12
45
|
const {
|
|
13
46
|
aside,
|
|
14
47
|
children,
|
|
15
48
|
column_width = '280px',
|
|
49
|
+
aside_label = 'menu',
|
|
16
50
|
class: class_name = '',
|
|
17
51
|
...rest
|
|
18
52
|
}: SvelteHTMLElements['div'] & {
|
|
@@ -23,24 +57,85 @@
|
|
|
23
57
|
* @default '280px'
|
|
24
58
|
*/
|
|
25
59
|
column_width?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The stacked toggle's text and the aside's accessible name.
|
|
62
|
+
* @default 'menu'
|
|
63
|
+
*/
|
|
64
|
+
aside_label?: string;
|
|
26
65
|
} = $props();
|
|
66
|
+
|
|
67
|
+
const uid = $props.id();
|
|
68
|
+
const aside_id = `${uid}-aside`;
|
|
69
|
+
|
|
70
|
+
let aside_open = $state(false);
|
|
71
|
+
|
|
72
|
+
let toggle_bar_el: HTMLDivElement;
|
|
73
|
+
let aside_el: HTMLElement;
|
|
74
|
+
|
|
75
|
+
const toggle_aside = async (): Promise<void> => {
|
|
76
|
+
aside_open = !aside_open;
|
|
77
|
+
await tick();
|
|
78
|
+
// closed, or closed again before the DOM settled
|
|
79
|
+
if (!aside_open) return;
|
|
80
|
+
// the aside goes in above the content, and scroll anchoring keeps the
|
|
81
|
+
// content where it was, so opening while scrolled - the stacked body,
|
|
82
|
+
// or the page past a toggle half out of view - leaves the aside above
|
|
83
|
+
// the view; bring its start back under the toggle, whether the toggle
|
|
84
|
+
// sticks (bounded) or sits just above it (growing)
|
|
85
|
+
const bar = toggle_bar_el.getBoundingClientRect();
|
|
86
|
+
if (aside_el.getBoundingClientRect().top >= Math.max(0, bar.bottom)) return;
|
|
87
|
+
aside_el.style.scrollMarginTop = `${bar.height}px`;
|
|
88
|
+
aside_el.scrollIntoView({ block: 'start' });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
afterNavigate(() => {
|
|
92
|
+
aside_open = false;
|
|
93
|
+
});
|
|
27
94
|
</script>
|
|
28
95
|
|
|
29
96
|
<div class="column-layout {class_name}" style:--column_width={column_width} {...rest}>
|
|
30
|
-
<
|
|
31
|
-
{
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
97
|
+
<div class={['column-layout-body', { 'aside-open': aside_open }]}>
|
|
98
|
+
<div class="column-toggle-bar" bind:this={toggle_bar_el}>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
class="column-toggle menuitem plain"
|
|
102
|
+
aria-expanded={aside_open}
|
|
103
|
+
aria-controls={aside_id}
|
|
104
|
+
onclick={toggle_aside}
|
|
105
|
+
>
|
|
106
|
+
{aside_label}
|
|
107
|
+
<span class="chevron" aria-hidden="true"></span>
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<aside
|
|
111
|
+
id={aside_id}
|
|
112
|
+
class="column-fixed unstyled"
|
|
113
|
+
aria-label={aside_label}
|
|
114
|
+
bind:this={aside_el}
|
|
115
|
+
>
|
|
116
|
+
{@render aside()}
|
|
117
|
+
</aside>
|
|
118
|
+
<div class="column-fluid">
|
|
119
|
+
{@render children()}
|
|
120
|
+
</div>
|
|
35
121
|
</div>
|
|
36
122
|
</div>
|
|
37
123
|
|
|
38
124
|
<style>
|
|
39
125
|
.column-layout {
|
|
126
|
+
container: column_layout / inline-size;
|
|
127
|
+
height: 100%;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.column-layout-body {
|
|
40
131
|
display: flex;
|
|
41
132
|
height: 100%;
|
|
42
133
|
}
|
|
43
134
|
|
|
135
|
+
.column-toggle-bar {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
|
|
44
139
|
.column-fixed {
|
|
45
140
|
width: var(--column_width, 280px);
|
|
46
141
|
min-width: var(--column_width, 280px);
|
|
@@ -54,4 +149,40 @@
|
|
|
54
149
|
min-width: 0;
|
|
55
150
|
overflow: auto;
|
|
56
151
|
}
|
|
152
|
+
|
|
153
|
+
/* fuz_css's `sm` breakpoint is the media query `40rem`, where `rem` is the
|
|
154
|
+
initial font size; in a container query `rem` reads the root, which
|
|
155
|
+
fuz_css sets to 62.5%, so `64rem` is the same width at any user font
|
|
156
|
+
size - keep in sync with `sm` in fuz_css's `modifiers.ts` */
|
|
157
|
+
@container column_layout (width < 64rem) {
|
|
158
|
+
.column-layout-body {
|
|
159
|
+
flex-direction: column;
|
|
160
|
+
overflow: auto;
|
|
161
|
+
}
|
|
162
|
+
.column-toggle-bar {
|
|
163
|
+
display: block;
|
|
164
|
+
position: sticky;
|
|
165
|
+
top: 0;
|
|
166
|
+
z-index: 1;
|
|
167
|
+
background-color: var(--column_toggle_bg, var(--shade_00));
|
|
168
|
+
}
|
|
169
|
+
/* `flex: none` because the aside scrolls, so it would otherwise shrink
|
|
170
|
+
to nothing in the body's fixed height */
|
|
171
|
+
.column-layout-body > .column-fixed {
|
|
172
|
+
flex: none;
|
|
173
|
+
width: auto;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
height: auto;
|
|
176
|
+
}
|
|
177
|
+
.column-layout-body:not(.aside-open) > .column-fixed {
|
|
178
|
+
display: none;
|
|
179
|
+
}
|
|
180
|
+
.column-fluid {
|
|
181
|
+
flex: none;
|
|
182
|
+
height: auto;
|
|
183
|
+
}
|
|
184
|
+
.column-layout-body.aside-open > .column-toggle-bar .chevron {
|
|
185
|
+
rotate: 90deg;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
57
188
|
</style>
|
|
@@ -1,10 +1,42 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Two-column layout
|
|
2
|
+
* Two-column layout - fixed-width `aside` on the left, fluid `children`
|
|
3
3
|
* column on the right. Both columns scroll independently.
|
|
4
4
|
*
|
|
5
|
+
* Below a threshold of the layout's own width - a container query, not a
|
|
6
|
+
* media query, so a nested layout decides for itself - the columns stack:
|
|
7
|
+
* the aside collapses behind a toggle button labelled `aside_label`, and
|
|
8
|
+
* when opened it sits in flow above the content, pushing it down, until
|
|
9
|
+
* the toggle closes it or a navigation does. Opening scrolls the aside
|
|
10
|
+
* into view, under the toggle, when it would land above the view.
|
|
11
|
+
*
|
|
12
|
+
* With a bounded height, the stacked layout scrolls as one column and the
|
|
13
|
+
* toggle sticks to its top; under a parent that grows with its content,
|
|
14
|
+
* the page scrolls and the toggle scrolls away with it.
|
|
15
|
+
*
|
|
16
|
+
* The threshold matches fuz_css's `sm` breakpoint. Above it the toggle is
|
|
17
|
+
* `display: none`. The switch is CSS alone, so the server render shows the
|
|
18
|
+
* right form at any width and hydration changes nothing.
|
|
19
|
+
*
|
|
20
|
+
* The root is an `inline-size` query container, so its width can't come
|
|
21
|
+
* from its content: a layout placed where it would shrink to fit (an
|
|
22
|
+
* inline-block, an absolutely positioned or floated box without a width,
|
|
23
|
+
* a flex row item that doesn't grow) collapses to zero width. Give it a
|
|
24
|
+
* width from its context, as a block or a stretched or growing flex item.
|
|
25
|
+
*
|
|
26
|
+
* Requires the SvelteKit runtime - it closes the aside from
|
|
27
|
+
* `afterNavigate`. Without JS the stacked aside can't be opened, since the
|
|
28
|
+
* toggle is client state; above the threshold it's always shown.
|
|
29
|
+
*
|
|
30
|
+
* Renders a generic `<aside>` named by `aside_label`; wrap navigation
|
|
31
|
+
* links in a `<nav>` inside the `aside` snippet.
|
|
32
|
+
*
|
|
33
|
+
* Styling hooks: `--column_width` (set from `column_width`) and
|
|
34
|
+
* `--column_toggle_bg`, the stacked toggle bar's opaque background so
|
|
35
|
+
* content scrolls under it unseen (default `--shade_00`, the page ground).
|
|
36
|
+
*
|
|
5
37
|
* @module
|
|
6
38
|
*/
|
|
7
|
-
import type
|
|
39
|
+
import { type Snippet } from 'svelte';
|
|
8
40
|
import type { SvelteHTMLElements } from 'svelte/elements';
|
|
9
41
|
type $$ComponentProps = SvelteHTMLElements['div'] & {
|
|
10
42
|
aside: Snippet;
|
|
@@ -14,6 +46,11 @@ type $$ComponentProps = SvelteHTMLElements['div'] & {
|
|
|
14
46
|
* @default '280px'
|
|
15
47
|
*/
|
|
16
48
|
column_width?: string;
|
|
49
|
+
/**
|
|
50
|
+
* The stacked toggle's text and the aside's accessible name.
|
|
51
|
+
* @default 'menu'
|
|
52
|
+
*/
|
|
53
|
+
aside_label?: string;
|
|
17
54
|
};
|
|
18
55
|
declare const ColumnLayout: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
19
56
|
type ColumnLayout = ReturnType<typeof ColumnLayout>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ColumnLayout.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/ColumnLayout.svelte"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"ColumnLayout.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/ColumnLayout.svelte"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCI;AACJ,OAAO,EAAQ,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG;IACpD,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAmEH,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fuzdev/fuz_app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.123.0",
|
|
4
4
|
"description": "fullstack app library",
|
|
5
5
|
"glyph": "🗝",
|
|
6
6
|
"logo": "logo.svg",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"@fuzdev/blake3-wasm": "^0.2.0",
|
|
81
81
|
"@fuzdev/fuz_code": "^0.50.0",
|
|
82
82
|
"@fuzdev/fuz_css": "^0.65.0",
|
|
83
|
-
"@fuzdev/fuz_ui": "^0.
|
|
83
|
+
"@fuzdev/fuz_ui": "^0.210.0",
|
|
84
84
|
"@fuzdev/fuz_util": "^0.71.0",
|
|
85
85
|
"@fuzdev/gro": "^0.213.0",
|
|
86
86
|
"@fuzdev/mdz": "^0.3.0",
|