@fuzdev/fuz_app 0.122.0 → 0.124.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/README.md CHANGED
@@ -14,6 +14,8 @@ fuz_app supports deploying with Deno, Node, and Bun,
14
14
  to servers, static websites, and local-first binaries, with more to come,
15
15
  eventually with compatible alternatives written in Rust.
16
16
 
17
+ This library has no AI features by design, for that see [zzz](https://github.com/fuzdev/zzz).
18
+
17
19
  For more see the <a href="https://github.com/fuzdev/fuz_app/discussions">discussions</a>.
18
20
  fuz_app is part of the Fuz stack
19
21
  ([fuz.dev](https://www.fuz.dev/), [@fuzdev](https://github.com/fuzdev)).
@@ -388,7 +388,9 @@
388
388
  --sidebar_offset: 0px;
389
389
  display: flex;
390
390
  flex-direction: column;
391
- min-height: 100vh;
391
+ /* the small viewport, so the floor alone never makes a mobile page
392
+ scroll with the toolbar shown, nor relayouts as the toolbar moves */
393
+ min-height: 100svh;
392
394
  }
393
395
  .app-shell.wide-open > .app-shell-content {
394
396
  --sidebar_offset: var(--sidebar_width);
package/dist/ui/CLAUDE.md CHANGED
@@ -221,7 +221,22 @@ 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'`, `toggle_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
+ `toggle_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 column is a plain `<div>` (the toggle's `aria-controls` target), not
236
+ a landmark, as with `AppShell`'s sidebar; consumers render the landmark
237
+ inside the snippet - a `<nav aria-label>` for links, or an `<aside aria-label>` for other content. Hooks: `--column_width`, `--column_toggle_bg` (default
238
+ `--shade_00`). The container can't size from its content, so a
239
+ shrink-to-fit placement collapses it to zero width.
225
240
  - `MenuLink.svelte` — SvelteKit `<a>` with `selected` derived from
226
241
  `page.url.pathname` (fires for exact match + descendant pages).
227
242
  Exact matches additionally get `aria-current="page"`. Takes `path`
@@ -1,18 +1,54 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * Two-column layout — fixed-width `aside` on the left, fluid `children`
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 `toggle_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
+ * The aside column is a plain `<div>`, not a landmark, as with `AppShell`'s
32
+ * sidebar: render the landmark inside the `aside` snippet - a
33
+ * `<nav aria-label>` for navigation links, or an `<aside aria-label>` for
34
+ * other content.
35
+ *
36
+ * Styling hooks: `--column_width` (set from `column_width`) and
37
+ * `--column_toggle_bg`, the stacked toggle bar's opaque background so
38
+ * content scrolls under it unseen (default `--shade_00`, the page ground).
39
+ *
6
40
  * @module
7
41
  */
8
42
 
9
- import type { Snippet } from 'svelte';
43
+ import { tick, type Snippet } from 'svelte';
10
44
  import type { SvelteHTMLElements } from 'svelte/elements';
45
+ import { afterNavigate } from '$app/navigation';
11
46
 
12
47
  const {
13
48
  aside,
14
49
  children,
15
50
  column_width = '280px',
51
+ toggle_label = 'menu',
16
52
  class: class_name = '',
17
53
  ...rest
18
54
  }: SvelteHTMLElements['div'] & {
@@ -23,24 +59,80 @@
23
59
  * @default '280px'
24
60
  */
25
61
  column_width?: string;
62
+ /**
63
+ * The stacked toggle's text.
64
+ * @default 'menu'
65
+ */
66
+ toggle_label?: string;
26
67
  } = $props();
68
+
69
+ const uid = $props.id();
70
+ const aside_id = `${uid}-aside`;
71
+
72
+ let aside_open = $state(false);
73
+
74
+ let toggle_bar_el: HTMLDivElement;
75
+ let aside_el: HTMLDivElement;
76
+
77
+ const toggle_aside = async (): Promise<void> => {
78
+ aside_open = !aside_open;
79
+ await tick();
80
+ // closed, or closed again before the DOM settled
81
+ if (!aside_open) return;
82
+ // the aside goes in above the content, and scroll anchoring keeps the
83
+ // content where it was, so opening while scrolled - the stacked body,
84
+ // or the page past a toggle half out of view - leaves the aside above
85
+ // the view; bring its start back under the toggle, whether the toggle
86
+ // sticks (bounded) or sits just above it (growing)
87
+ const bar = toggle_bar_el.getBoundingClientRect();
88
+ if (aside_el.getBoundingClientRect().top >= Math.max(0, bar.bottom)) return;
89
+ aside_el.style.scrollMarginTop = `${bar.height}px`;
90
+ aside_el.scrollIntoView({ block: 'start' });
91
+ };
92
+
93
+ afterNavigate(() => {
94
+ aside_open = false;
95
+ });
27
96
  </script>
28
97
 
29
98
  <div class="column-layout {class_name}" style:--column_width={column_width} {...rest}>
30
- <aside class="column-fixed unstyled">
31
- {@render aside()}
32
- </aside>
33
- <div class="column-fluid">
34
- {@render children()}
99
+ <div class={['column-layout-body', { 'aside-open': aside_open }]}>
100
+ <div class="column-toggle-bar" bind:this={toggle_bar_el}>
101
+ <button
102
+ type="button"
103
+ class="column-toggle menuitem plain"
104
+ aria-expanded={aside_open}
105
+ aria-controls={aside_id}
106
+ onclick={toggle_aside}
107
+ >
108
+ {toggle_label}
109
+ <span class="chevron" aria-hidden="true"></span>
110
+ </button>
111
+ </div>
112
+ <div id={aside_id} class="column-fixed" bind:this={aside_el}>
113
+ {@render aside()}
114
+ </div>
115
+ <div class="column-fluid">
116
+ {@render children()}
117
+ </div>
35
118
  </div>
36
119
  </div>
37
120
 
38
121
  <style>
39
122
  .column-layout {
123
+ container: column_layout / inline-size;
124
+ height: 100%;
125
+ }
126
+
127
+ .column-layout-body {
40
128
  display: flex;
41
129
  height: 100%;
42
130
  }
43
131
 
132
+ .column-toggle-bar {
133
+ display: none;
134
+ }
135
+
44
136
  .column-fixed {
45
137
  width: var(--column_width, 280px);
46
138
  min-width: var(--column_width, 280px);
@@ -54,4 +146,40 @@
54
146
  min-width: 0;
55
147
  overflow: auto;
56
148
  }
149
+
150
+ /* fuz_css's `sm` breakpoint is the media query `40rem`, where `rem` is the
151
+ initial font size; in a container query `rem` reads the root, which
152
+ fuz_css sets to 62.5%, so `64rem` is the same width at any user font
153
+ size - keep in sync with `sm` in fuz_css's `modifiers.ts` */
154
+ @container column_layout (width < 64rem) {
155
+ .column-layout-body {
156
+ flex-direction: column;
157
+ overflow: auto;
158
+ }
159
+ .column-toggle-bar {
160
+ display: block;
161
+ position: sticky;
162
+ top: 0;
163
+ z-index: 1;
164
+ background-color: var(--column_toggle_bg, var(--shade_00));
165
+ }
166
+ /* `flex: none` because the aside scrolls, so it would otherwise shrink
167
+ to nothing in the body's fixed height */
168
+ .column-layout-body > .column-fixed {
169
+ flex: none;
170
+ width: auto;
171
+ min-width: 0;
172
+ height: auto;
173
+ }
174
+ .column-layout-body:not(.aside-open) > .column-fixed {
175
+ display: none;
176
+ }
177
+ .column-fluid {
178
+ flex: none;
179
+ height: auto;
180
+ }
181
+ .column-layout-body.aside-open > .column-toggle-bar .chevron {
182
+ rotate: 90deg;
183
+ }
184
+ }
57
185
  </style>
@@ -1,10 +1,44 @@
1
1
  /**
2
- * Two-column layout — fixed-width `aside` on the left, fluid `children`
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 `toggle_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
+ * The aside column is a plain `<div>`, not a landmark, as with `AppShell`'s
31
+ * sidebar: render the landmark inside the `aside` snippet - a
32
+ * `<nav aria-label>` for navigation links, or an `<aside aria-label>` for
33
+ * other content.
34
+ *
35
+ * Styling hooks: `--column_width` (set from `column_width`) and
36
+ * `--column_toggle_bg`, the stacked toggle bar's opaque background so
37
+ * content scrolls under it unseen (default `--shade_00`, the page ground).
38
+ *
5
39
  * @module
6
40
  */
7
- import type { Snippet } from 'svelte';
41
+ import { type Snippet } from 'svelte';
8
42
  import type { SvelteHTMLElements } from 'svelte/elements';
9
43
  type $$ComponentProps = SvelteHTMLElements['div'] & {
10
44
  aside: Snippet;
@@ -14,6 +48,11 @@ type $$ComponentProps = SvelteHTMLElements['div'] & {
14
48
  * @default '280px'
15
49
  */
16
50
  column_width?: string;
51
+ /**
52
+ * The stacked toggle's text.
53
+ * @default 'menu'
54
+ */
55
+ toggle_label?: string;
17
56
  };
18
57
  declare const ColumnLayout: import("svelte").Component<$$ComponentProps, {}, "">;
19
58
  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;;;;;OAKI;AACJ,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,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;CACtB,CAAC;AA6BH,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ColumnLayout.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/ui/ColumnLayout.svelte"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAuCI;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,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,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.122.0",
3
+ "version": "0.124.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",
@@ -79,8 +79,8 @@
79
79
  "@electric-sql/pglite": "^0.4.5",
80
80
  "@fuzdev/blake3-wasm": "^0.2.0",
81
81
  "@fuzdev/fuz_code": "^0.50.0",
82
- "@fuzdev/fuz_css": "^0.65.0",
83
- "@fuzdev/fuz_ui": "^0.209.0",
82
+ "@fuzdev/fuz_css": "^0.65.1",
83
+ "@fuzdev/fuz_ui": "^0.210.1",
84
84
  "@fuzdev/fuz_util": "^0.71.0",
85
85
  "@fuzdev/gro": "^0.213.0",
86
86
  "@fuzdev/mdz": "^0.3.0",