@fuzdev/fuz_ui 0.210.4 → 0.212.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/Alert.svelte CHANGED
@@ -22,7 +22,8 @@
22
22
  div_attrs,
23
23
  children,
24
24
  ...rest
25
- }: // generic element attrs, the common denominator of the rendered roots - // branch-specific attributes go in `button_attrs`/`div_attrs`
25
+ }: // generic element attrs, the common denominator of the rendered roots;
26
+ // branch-specific attributes go in `button_attrs` and `div_attrs`
26
27
  HTMLAttributes<HTMLElement> & {
27
28
  status?: AlertStatus;
28
29
  color?: string;
@@ -32,7 +33,11 @@
32
33
  /** Renders the alert as a `<button>` when provided. */
33
34
  onclick?: (() => void) | undefined;
34
35
  disabled?: boolean;
35
- icon?: string | Snippet<[icon: string]> | null | undefined; // TODO experimenting with this, gets complex in the impl
36
+ /**
37
+ * A decorative icon, hidden from assistive tech: a string, or a snippet given the
38
+ * status's icon. Defaults to the status's icon, and `null` or an empty string is none.
39
+ */
40
+ icon?: string | Snippet<[icon: string]> | null | undefined;
36
41
  /** Button attributes, applied only when `onclick` renders the alert as a `<button>`. */
37
42
  button_attrs?: SvelteHTMLElements['button'];
38
43
  /** Div attributes, applied only when the alert renders as a `<div>` (no `onclick`). */
@@ -44,9 +49,7 @@
44
49
  // TODO change this to use the hue and put transparency on the borders, or add a borderColor option
45
50
  const { color: status_color, icon: status_icon } = $derived(options);
46
51
  const final_color = $derived(color ?? status_color);
47
- const final_icon = $derived(
48
- typeof icon === 'string' ? icon : (status_icon ?? alert_status_options.inform.icon!)
49
- );
52
+ const final_icon = $derived(typeof icon === 'string' ? icon : status_icon);
50
53
  </script>
51
54
 
52
55
  {#if onclick}
@@ -59,7 +62,7 @@
59
62
  {onclick}
60
63
  disabled={disabled ?? button_attrs?.disabled}
61
64
  >
62
- {@render content()}
65
+ {@render alert_content()}
63
66
  </button>
64
67
  {:else}
65
68
  <div
@@ -69,21 +72,21 @@
69
72
  class="alert panel {rest.class} {div_attrs?.class}"
70
73
  style:--text_color={final_color}
71
74
  >
72
- {@render content()}
75
+ {@render alert_content()}
73
76
  </div>
74
77
  {/if}
75
78
 
76
- {#snippet content()}
77
- {#if icon !== null}
78
- <div class="icon">
79
- {#if !icon || typeof icon === 'string'}
80
- {final_icon}
81
- {:else}
79
+ {#snippet alert_content()}
80
+ <!-- the status's icon by default, or one given, and none for `null` or an empty string -->
81
+ {#if icon === undefined || icon}
82
+ <div class="icon" aria-hidden="true">
83
+ {#if typeof icon === 'function'}
82
84
  {@render icon(final_icon)}
85
+ {:else}
86
+ {final_icon}
83
87
  {/if}
84
88
  </div>
85
89
  {/if}
86
- <!-- TODO conflict in the names of content as the snippet and this inner div -->
87
90
  <div class="content">
88
91
  {@render children()}
89
92
  </div>
@@ -1,13 +1,17 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes, SvelteHTMLElements } from 'svelte/elements';
3
3
  import { type AlertStatus } from './alert.js';
4
- type $$ComponentProps = // generic element attrs, the common denominator of the rendered roots - // branch-specific attributes go in `button_attrs`/`div_attrs`
4
+ type $$ComponentProps = // generic element attrs, the common denominator of the rendered roots;
5
5
  HTMLAttributes<HTMLElement> & {
6
6
  status?: AlertStatus;
7
7
  color?: string;
8
8
  /** Renders the alert as a `<button>` when provided. */
9
9
  onclick?: (() => void) | undefined;
10
10
  disabled?: boolean;
11
+ /**
12
+ * A decorative icon, hidden from assistive tech: a string, or a snippet given the
13
+ * status's icon. Defaults to the status's icon, and `null` or an empty string is none.
14
+ */
11
15
  icon?: string | Snippet<[icon: string]> | null | undefined;
12
16
  /** Button attributes, applied only when `onclick` renders the alert as a `<button>`. */
13
17
  button_attrs?: SvelteHTMLElements['button'];
@@ -1 +1 @@
1
- {"version":3,"file":"Alert.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Alert.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAEnE,KAAK,gBAAgB,GACpB,AADwB,uIAAuI;AAC/J,cAAc,CAAC,WAAW,CAAC,GAAG;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IAIf,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3D,wFAAwF;IACxF,YAAY,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC5C,uFAAuF;IACvF,SAAS,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACtC,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAkEJ,QAAA,MAAM,KAAK,sDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
1
+ {"version":3,"file":"Alert.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Alert.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE1E,OAAO,EAAwB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAEnE,KAAK,gBAAgB,GAEpB,AAFwB,uEAAuE;AAE/F,cAAc,CAAC,WAAW,CAAC,GAAG;IAC7B,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IAIf,uDAAuD;IACvD,OAAO,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,GAAG,IAAI,GAAG,SAAS,CAAC;IAC3D,wFAAwF;IACxF,YAAY,CAAC,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC5C,uFAAuF;IACvF,SAAS,CAAC,EAAE,kBAAkB,CAAC,KAAK,CAAC,CAAC;IACtC,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAgEJ,QAAA,MAAM,KAAK,sDAAwC,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
@@ -12,6 +12,7 @@
12
12
  const {
13
13
  path,
14
14
  selected_path,
15
+ root_path,
15
16
  base_path,
16
17
  separator,
17
18
  children,
@@ -26,6 +27,12 @@
26
27
  * `null` means none and `undefined` is detected from the current url.
27
28
  */
28
29
  selected_path?: string | null | undefined;
30
+ /**
31
+ * Where the root link goes, prefixed with a slash and relative to the base path,
32
+ * e.g. `'/docs'` on a home page, where linking to the root would link to itself.
33
+ * Defaults to the root.
34
+ */
35
+ root_path?: string;
29
36
  /**
30
37
  * Sets a custom base path of `path` and `selected_path`.
31
38
  * Defaults to `base` from `$app/paths`.
@@ -53,7 +60,9 @@
53
60
 
54
61
  const path_pieces = $derived(parse_path_pieces(final_path));
55
62
 
56
- const root_path = $derived(ensure_end(final_base_path, '/'));
63
+ const root_href = $derived(
64
+ root_path === undefined ? ensure_end(final_base_path, '/') : final_base_path + root_path
65
+ );
57
66
 
58
67
  // TODO animate these
59
68
  // `transition:slide={{axis: 'x'}}`
@@ -61,7 +70,7 @@
61
70
 
62
71
  <div {...rest} class="breadcrumb {rest.class}">
63
72
  <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
64
- <a href={root_path} class={{ selected: root_path === final_base_path + final_selected_path }}>
73
+ <a href={root_href} class={{ selected: root_href === final_base_path + final_selected_path }}>
65
74
  {#if children}
66
75
  {@render children()}
67
76
  {:else if icon_data}
@@ -94,12 +103,15 @@
94
103
  align-items: center;
95
104
  font-size: var(--font_size, var(--font_size_md));
96
105
  }
106
+ /* at least one line of text tall, so a root link holding only an icon is as tall as a
107
+ * breadcrumb with path pieces, and the layout around it doesn't shift between pages */
97
108
  a {
98
109
  position: relative;
99
110
  z-index: 1;
100
111
  padding: 0 calc(var(--font_size, var(--font_size_md)) * 0.5);
101
112
  display: flex;
102
113
  align-items: center;
114
+ min-height: 1lh;
103
115
  }
104
116
  .separator {
105
117
  position: relative;
@@ -10,6 +10,12 @@ type $$ComponentProps = SvelteHTMLElements['div'] & {
10
10
  * `null` means none and `undefined` is detected from the current url.
11
11
  */
12
12
  selected_path?: string | null | undefined;
13
+ /**
14
+ * Where the root link goes, prefixed with a slash and relative to the base path,
15
+ * e.g. `'/docs'` on a home page, where linking to the root would link to itself.
16
+ * Defaults to the root.
17
+ */
18
+ root_path?: string;
13
19
  /**
14
20
  * Sets a custom base path of `path` and `selected_path`.
15
21
  * Defaults to `base` from `$app/paths`.
@@ -1 +1 @@
1
- {"version":3,"file":"Breadcrumb.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Breadcrumb.svelte"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKzD,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG;IACpD;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1C;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAoEH,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"Breadcrumb.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Breadcrumb.svelte"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAKzD,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,KAAK,CAAC,GAAG;IACpD;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC1C;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAuEH,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
package/dist/Card.svelte CHANGED
@@ -4,30 +4,47 @@
4
4
  import type { Snippet } from 'svelte';
5
5
  import type { HTMLAttributes, SvelteHTMLElements } from 'svelte/elements';
6
6
 
7
+ import { href_is_current_page } from './page_helpers.js';
8
+
7
9
  // TODO think through Alert+Card APIs together, one can be a button and the other a link atm
8
10
 
9
11
  const {
10
12
  href,
11
13
  tag,
12
- align = 'left',
14
+ align = 'start',
13
15
  icon,
14
16
  a_attrs,
15
17
  children,
16
18
  ...rest
17
- }: // generic element attrs, the common denominator of the rendered roots - // branch-specific attributes go in `a_attrs`
19
+ }: // generic element attrs, the common denominator of the rendered roots;
20
+ // branch-specific attributes go in `a_attrs`
18
21
  HTMLAttributes<HTMLElement> & {
19
- /** Renders the card as an `<a>` when provided. */
22
+ /**
23
+ * Renders the card as an `<a>`, marked `selected` while it leads to the current
24
+ * page, ignoring a trailing slash, the query, and the hash.
25
+ */
20
26
  href?: string | undefined;
21
- tag?: string | undefined;
22
- align?: 'left' | 'right' | 'above' | 'below';
23
- icon?: string | Snippet;
27
+ /** The element to render, defaulting to `a` with an `href` and `div` without. */
28
+ tag?: keyof HTMLElementTagNameMap | undefined;
29
+ /**
30
+ * The card's layout: where the icon sits, at the inline start or end beside the
31
+ * content or above or below it, and how the content aligns, toward the end for
32
+ * `end` and centered for `top` and `bottom`.
33
+ * @default 'start'
34
+ */
35
+ align?: 'start' | 'end' | 'top' | 'bottom';
36
+ /**
37
+ * A decorative icon, hidden from assistive tech: a string like an emoji or a glyph,
38
+ * or a snippet for anything else. None when absent, null, or empty.
39
+ */
40
+ icon?: string | Snippet | null;
24
41
  /** Anchor attributes, applied only when `href` renders the card as an `<a>`. */
25
42
  a_attrs?: SvelteHTMLElements['a'];
26
43
  children: Snippet;
27
44
  } = $props();
28
45
 
29
46
  const link = $derived(!!href);
30
- const selected = $derived(link && page.url.pathname === href);
47
+ const selected = $derived(!!href && href_is_current_page(href, page.url));
31
48
  // the tag renders through `svelte:element`, so declare the styled one for fuz_css extraction
32
49
  // @fuz-elements a
33
50
  const final_tag = $derived(tag ?? (link ? 'a' : 'div'));
@@ -43,41 +60,38 @@
43
60
  }
44
61
  });
45
62
  }
46
-
47
- const left = $derived(align === 'left');
48
- const right = $derived(align === 'right');
49
- const above = $derived(align === 'above');
50
- const below = $derived(align === 'below');
51
-
52
- const final_icon: string | Snippet = $derived(icon ?? (link ? '🔗' : '🪧'));
53
63
  </script>
54
64
 
65
+ <!-- a button that isn't told otherwise doesn't submit a form around it -->
55
66
  <svelte:element
56
67
  this={final_tag}
68
+ type={final_tag === 'button' ? 'button' : undefined}
57
69
  {...rest}
58
70
  {...inferred_attrs}
59
- class={['card', rest.class, inferred_attrs?.class, { link, selected, left, right, above, below }]}
71
+ class={['card', align, rest.class, inferred_attrs?.class, { link, selected }]}
60
72
  >
61
- {#if align === 'left' || align === 'above'}
73
+ {#if align === 'start' || align === 'top'}
62
74
  {@render icon_snippet()}
63
75
  {/if}
64
76
  <div class="content">
65
77
  {@render children()}
66
78
  </div>
67
- {#if align === 'right' || align === 'below'}
79
+ {#if align === 'end' || align === 'bottom'}
68
80
  {@render icon_snippet()}
69
81
  {/if}
70
82
  </svelte:element>
71
83
 
72
- <!-- TODO name? -->
84
+ <!-- an empty string is no icon, so no empty element takes the icon's margin -->
73
85
  {#snippet icon_snippet()}
74
- <div class="icon">
75
- {#if typeof final_icon === 'string'}
76
- {final_icon}
77
- {:else}
78
- {@render final_icon()}
79
- {/if}
80
- </div>
86
+ {#if icon}
87
+ <div class="icon" aria-hidden="true">
88
+ {#if typeof icon === 'string'}
89
+ {icon}
90
+ {:else}
91
+ {@render icon()}
92
+ {/if}
93
+ </div>
94
+ {/if}
81
95
  {/snippet}
82
96
 
83
97
  <style>
@@ -92,13 +106,13 @@
92
106
  background-color: var(--fg_10);
93
107
  border-radius: var(--border_radius, var(--border_radius_md));
94
108
  text-decoration: none;
95
- text-align: left;
109
+ text-align: start;
96
110
  }
97
- .right {
111
+ .end {
98
112
  justify-content: flex-end;
99
113
  }
100
- .above,
101
- .below {
114
+ .top,
115
+ .bottom {
102
116
  flex-direction: column;
103
117
  text-align: center;
104
118
  }
@@ -128,12 +142,15 @@
128
142
  .link:hover .content {
129
143
  text-decoration: underline;
130
144
  }
131
- .left .content {
132
- padding-right: var(--space_sm);
145
+ .end .content {
146
+ text-align: end;
147
+ }
148
+ /* a little room on the side away from the icon */
149
+ .start:has(> .icon) .content {
150
+ padding-inline-end: var(--space_sm);
133
151
  }
134
- .right .content {
135
- text-align: right;
136
- padding-left: var(--space_sm);
152
+ .end:has(> .icon) .content {
153
+ padding-inline-start: var(--space_sm);
137
154
  }
138
155
  .icon {
139
156
  font-size: var(--icon_size, var(--icon_size_md));
@@ -141,17 +158,16 @@
141
158
  display: flex;
142
159
  justify-content: center;
143
160
  }
144
- /* TODO @many remove all :global usage after https://github.com/sveltejs/svelte/issues/10143 */
145
- .left :global(.icon) {
146
- margin-right: var(--icon_margin);
161
+ .start .icon {
162
+ margin-inline-end: var(--icon_margin);
147
163
  }
148
- .right :global(.icon) {
149
- margin-left: var(--icon_margin);
164
+ .end .icon {
165
+ margin-inline-start: var(--icon_margin);
150
166
  }
151
- .above :global(.icon) {
167
+ .top .icon {
152
168
  margin-bottom: var(--icon_margin);
153
169
  }
154
- .below :global(.icon) {
170
+ .bottom .icon {
155
171
  margin-top: var(--icon_margin);
156
172
  }
157
173
  @media (max-width: 460px) {
@@ -159,19 +175,12 @@
159
175
  font-size: var(--font_size_xl);
160
176
  }
161
177
  }
178
+ /* the icon's size and margin come from the variables, whichever side it is on */
162
179
  @media (max-width: 380px) {
163
180
  .card {
164
181
  --icon_size: var(--icon_size_md);
165
182
  --icon_margin: var(--space_sm);
166
183
  font-size: var(--font_size_lg);
167
184
  }
168
- .icon {
169
- font-size: var(--icon_size_md);
170
- margin-right: var(--space_sm);
171
- }
172
- .right :global(.icon) {
173
- margin-right: 0;
174
- margin-left: var(--space_sm);
175
- }
176
185
  }
177
186
  </style>
@@ -1,12 +1,26 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes, SvelteHTMLElements } from 'svelte/elements';
3
- type $$ComponentProps = // generic element attrs, the common denominator of the rendered roots - // branch-specific attributes go in `a_attrs`
3
+ type $$ComponentProps = // generic element attrs, the common denominator of the rendered roots;
4
4
  HTMLAttributes<HTMLElement> & {
5
- /** Renders the card as an `<a>` when provided. */
5
+ /**
6
+ * Renders the card as an `<a>`, marked `selected` while it leads to the current
7
+ * page, ignoring a trailing slash, the query, and the hash.
8
+ */
6
9
  href?: string | undefined;
7
- tag?: string | undefined;
8
- align?: 'left' | 'right' | 'above' | 'below';
9
- icon?: string | Snippet;
10
+ /** The element to render, defaulting to `a` with an `href` and `div` without. */
11
+ tag?: keyof HTMLElementTagNameMap | undefined;
12
+ /**
13
+ * The card's layout: where the icon sits, at the inline start or end beside the
14
+ * content or above or below it, and how the content aligns, toward the end for
15
+ * `end` and centered for `top` and `bottom`.
16
+ * @default 'start'
17
+ */
18
+ align?: 'start' | 'end' | 'top' | 'bottom';
19
+ /**
20
+ * A decorative icon, hidden from assistive tech: a string like an emoji or a glyph,
21
+ * or a snippet for anything else. None when absent, null, or empty.
22
+ */
23
+ icon?: string | Snippet | null;
10
24
  /** Anchor attributes, applied only when `href` renders the card as an `<a>`. */
11
25
  a_attrs?: SvelteHTMLElements['a'];
12
26
  children: Snippet;
@@ -1 +1 @@
1
- {"version":3,"file":"Card.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Card.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAEzE,KAAK,gBAAgB,GACpB,AADwB,sHAAsH;AAC9I,cAAc,CAAC,WAAW,CAAC,GAAG;IAC7B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC7C,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,gFAAgF;IAChF,OAAO,CAAC,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAwEJ,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"Card.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Card.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAIzE,KAAK,gBAAgB,GAEpB,AAFwB,uEAAuE;AAE/F,cAAc,CAAC,WAAW,CAAC,GAAG;IAC7B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,iFAAiF;IACjF,GAAG,CAAC,EAAE,MAAM,qBAAqB,GAAG,SAAS,CAAC;IAC9C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC3C;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IAC/B,gFAAgF;IAChF,OAAO,CAAC,EAAE,kBAAkB,CAAC,GAAG,CAAC,CAAC;IAClC,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAsEJ,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -6,6 +6,7 @@
6
6
 
7
7
  import { contextmenu_context } from './contextmenu_state.svelte.js';
8
8
  import { icon_external_link, icon_link } from './icons.js';
9
+ import { href_is_current_page } from './page_helpers.js';
9
10
  import type { SvgData } from './svg.js';
10
11
  import Svg from './Svg.svelte';
11
12
  import ContextmenuIcon from './ContextmenuIcon.svelte';
@@ -53,7 +54,7 @@
53
54
  const external = $derived(!(text[0] === '.' || (text[0] === '/' && text[1] !== '/')));
54
55
  const rel = $derived(external ? external_rel : undefined);
55
56
 
56
- const current_page = $derived(page.url.pathname === href);
57
+ const current_page = $derived(href_is_current_page(href, page.url));
57
58
  </script>
58
59
 
59
60
  <li role="none">
@@ -1 +1 @@
1
- {"version":3,"file":"ContextmenuLinkEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuLinkEntry.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAKtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAIvC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA2FH,QAAA,MAAM,oBAAoB,sDAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"ContextmenuLinkEntry.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/ContextmenuLinkEntry.svelte"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AAIvC,KAAK,gBAAgB,GAAI;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,OAAO,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA4FH,QAAA,MAAM,oBAAoB,sDAAwC,CAAC;AACnE,KAAK,oBAAoB,GAAG,UAAU,CAAC,OAAO,oBAAoB,CAAC,CAAC;AACpE,eAAe,oBAAoB,CAAC"}
package/dist/Docs.svelte CHANGED
@@ -18,9 +18,30 @@
18
18
 
19
19
  const {
20
20
  tomes,
21
+ primary_nav,
22
+ secondary_nav_header,
23
+ secondary_nav_footer,
24
+ footer,
21
25
  children
22
26
  }: {
23
27
  tomes: Array<Tome>;
28
+ /**
29
+ * Rendered in the primary nav after the breadcrumb, at its end, e.g. links to the
30
+ * site's other sections.
31
+ */
32
+ primary_nav?: Snippet;
33
+ /**
34
+ * Rendered above the tome menu in the secondary nav, in the sidebar and in the
35
+ * menu dialog of narrow screens, where the primary nav has little room.
36
+ */
37
+ secondary_nav_header?: Snippet;
38
+ /** Rendered below the tome menu in the secondary nav, in the sidebar and in the menu dialog. */
39
+ secondary_nav_footer?: Snippet;
40
+ /**
41
+ * Replaces the default footer, a `DocsFooter` with the site's repo and a link to
42
+ * fuz.dev, e.g. for a site that isn't part of the Fuz stack.
43
+ */
44
+ footer?: Snippet;
24
45
  children: Snippet;
25
46
  } = $props();
26
47
 
@@ -52,6 +73,11 @@
52
73
 
53
74
  <div class="docs" style:--docs_menu_width={docs_menu_width}>
54
75
  <DocsPrimaryNav>
76
+ {#if primary_nav}
77
+ <div class="primary-nav-end">
78
+ {@render primary_nav()}
79
+ </div>
80
+ {/if}
55
81
  <div class="nav-dialog-toggle">
56
82
  <button class="plain" type="button" onclick={() => toggle_secondary_nav_dialog()}>
57
83
  menu
@@ -61,7 +87,7 @@
61
87
  <!-- TODO @many dialog navs -->
62
88
  {#if !innerWidth.current || innerWidth.current > SECONDARY_NAV_BREAKPOINT}
63
89
  <div class="secondary-nav-wrapper">
64
- <DocsSecondaryNav {tomes} />
90
+ <DocsSecondaryNav {tomes} header={secondary_nav_header} footer={secondary_nav_footer} />
65
91
  </div>
66
92
  {/if}
67
93
  <main>
@@ -73,11 +99,15 @@
73
99
  <DocsTertiaryNav {tomes} {tomes_by_slug} />
74
100
  {/if}
75
101
  <section class="box">
76
- <DocsFooter repo_url={site.repo_url} root_url={FUZ_DEV_URL}>
77
- <div class="mb_xl5">
78
- <Breadcrumb />
79
- </div>
80
- </DocsFooter>
102
+ {#if footer}
103
+ {@render footer()}
104
+ {:else}
105
+ <DocsFooter repo_url={site.repo_url} root_url={FUZ_DEV_URL}>
106
+ <div class="mb_xl5">
107
+ <Breadcrumb />
108
+ </div>
109
+ </DocsFooter>
110
+ {/if}
81
111
  </section>
82
112
  </main>
83
113
  </div>
@@ -94,7 +124,12 @@
94
124
  <Breadcrumb />
95
125
  </div>
96
126
  <div class="px_lg pb_xl">
97
- <DocsSecondaryNav {tomes} sidebar={false} />
127
+ <DocsSecondaryNav
128
+ {tomes}
129
+ sidebar={false}
130
+ header={secondary_nav_header}
131
+ footer={secondary_nav_footer}
132
+ />
98
133
  <DocsTertiaryNav {tomes} {tomes_by_slug} sidebar={false} />
99
134
  </div>
100
135
  </div>
@@ -131,6 +166,12 @@
131
166
  display: contents;
132
167
  }
133
168
 
169
+ .primary-nav-end {
170
+ display: flex;
171
+ align-items: center;
172
+ gap: var(--space_md);
173
+ padding: 0 var(--space_md);
174
+ }
134
175
  .nav-dialog-toggle {
135
176
  display: none;
136
177
  }
@@ -2,6 +2,23 @@ import type { Snippet } from 'svelte';
2
2
  import { Tome } from './tome.js';
3
3
  type $$ComponentProps = {
4
4
  tomes: Array<Tome>;
5
+ /**
6
+ * Rendered in the primary nav after the breadcrumb, at its end, e.g. links to the
7
+ * site's other sections.
8
+ */
9
+ primary_nav?: Snippet;
10
+ /**
11
+ * Rendered above the tome menu in the secondary nav, in the sidebar and in the
12
+ * menu dialog of narrow screens, where the primary nav has little room.
13
+ */
14
+ secondary_nav_header?: Snippet;
15
+ /** Rendered below the tome menu in the secondary nav, in the sidebar and in the menu dialog. */
16
+ secondary_nav_footer?: Snippet;
17
+ /**
18
+ * Replaces the default footer, a `DocsFooter` with the site's repo and a link to
19
+ * fuz.dev, e.g. for a site that isn't part of the Fuz stack.
20
+ */
21
+ footer?: Snippet;
5
22
  children: Snippet;
6
23
  };
7
24
  declare const Docs: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"Docs.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Docs.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,OAAO,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAW/C,KAAK,gBAAgB,GAAI;IACxB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAyGH,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
1
+ {"version":3,"file":"Docs.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/Docs.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAMtC,OAAO,EAAE,IAAI,EAAiB,MAAM,WAAW,CAAC;AAW/C,KAAK,gBAAgB,GAAI;IACxB,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,gGAAgG;IAChG,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,OAAO,CAAC;CAClB,CAAC;AAsHH,QAAA,MAAM,IAAI,sDAAwC,CAAC;AACnD,KAAK,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC;AACpC,eAAe,IAAI,CAAC"}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { SvelteHTMLElements } from 'svelte/elements';
3
4
 
4
5
  import DocsMenu from './DocsMenu.svelte';
@@ -7,9 +8,15 @@
7
8
  const {
8
9
  tomes,
9
10
  sidebar = true,
11
+ header,
12
+ footer,
10
13
  ...rest
11
14
  }: SvelteHTMLElements['aside'] & {
12
15
  tomes: Array<Tome>;
16
+ /** Rendered above the menu, e.g. links to the site's other sections. */
17
+ header?: Snippet;
18
+ /** Rendered below the menu. */
19
+ footer?: Snippet;
13
20
  sidebar?: boolean; // TODO @many dialog navs (this shouldn't exist)
14
21
  } = $props();
15
22
 
@@ -18,7 +25,9 @@
18
25
 
19
26
  <aside {...rest} class={['docs-secondary-nav unstyled', rest.class, { sidebar }]}>
20
27
  <nav aria-label="Secondary nav">
28
+ {@render header?.()}
21
29
  <DocsMenu {tomes} {expand_width} />
30
+ {@render footer?.()}
22
31
  </nav>
23
32
  </aside>
24
33
 
@@ -1,7 +1,12 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { SvelteHTMLElements } from 'svelte/elements';
2
3
  import { Tome } from './tome.js';
3
4
  type $$ComponentProps = SvelteHTMLElements['aside'] & {
4
5
  tomes: Array<Tome>;
6
+ /** Rendered above the menu, e.g. links to the site's other sections. */
7
+ header?: Snippet;
8
+ /** Rendered below the menu. */
9
+ footer?: Snippet;
5
10
  sidebar?: boolean;
6
11
  };
7
12
  declare const DocsSecondaryNav: import("svelte").Component<$$ComponentProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"DocsSecondaryNav.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/DocsSecondaryNav.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEhC,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,OAAO,CAAC,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AA0BH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"DocsSecondaryNav.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/DocsSecondaryNav.svelte"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAG1D,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEhC,KAAK,gBAAgB,GAAI,kBAAkB,CAAC,OAAO,CAAC,GAAG;IACtD,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACnB,wEAAwE;IACxE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AA+BH,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
package/dist/alert.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type AlertStatus = 'inform' | 'help' | 'error';
2
2
  export interface AlertStatusOptions {
3
3
  color: string;
4
- icon: string | null;
4
+ icon: string;
5
5
  }
6
6
  export declare const alert_status_options: Record<AlertStatus, AlertStatusOptions>;
7
7
  //# sourceMappingURL=alert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"alert.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/alert.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACpB;AAED,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,WAAW,EAAE,kBAAkB,CAIxE,CAAC"}
1
+ {"version":3,"file":"alert.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/alert.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAEtD,MAAM,WAAW,kBAAkB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACb;AAED,eAAO,MAAM,oBAAoB,EAAE,MAAM,CAAC,WAAW,EAAE,kBAAkB,CAIxE,CAAC"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Helpers for relating links to the current page.
3
+ *
4
+ * @module
5
+ */
6
+ /**
7
+ * Whether `href` leads to the page at `url`: the same origin and pathname, with a
8
+ * trailing slash, the query, and the hash ignored. A relative `href` resolves
9
+ * against `url`, as a browser resolves it.
10
+ *
11
+ * @param href - a link's `href`, absolute or relative
12
+ * @param url - the current page's url, e.g. `page.url`
13
+ * @returns `false` for an `href` that isn't a valid url
14
+ */
15
+ export declare const href_is_current_page: (href: string, url: URL) => boolean;
16
+ //# sourceMappingURL=page_helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"page_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../src/lib/page_helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAAI,MAAM,MAAM,EAAE,KAAK,GAAG,KAAG,OAO7D,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Helpers for relating links to the current page.
3
+ *
4
+ * @module
5
+ */
6
+ /** A pathname without its trailing slash, except the root's. */
7
+ const strip_trailing_slash = (pathname) => pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
8
+ /**
9
+ * Whether `href` leads to the page at `url`: the same origin and pathname, with a
10
+ * trailing slash, the query, and the hash ignored. A relative `href` resolves
11
+ * against `url`, as a browser resolves it.
12
+ *
13
+ * @param href - a link's `href`, absolute or relative
14
+ * @param url - the current page's url, e.g. `page.url`
15
+ * @returns `false` for an `href` that isn't a valid url
16
+ */
17
+ export const href_is_current_page = (href, url) => {
18
+ if (!URL.canParse(href, url))
19
+ return false;
20
+ const target = new URL(href, url);
21
+ return (target.origin === url.origin &&
22
+ strip_trailing_slash(target.pathname) === strip_trailing_slash(url.pathname));
23
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_ui",
3
- "version": "0.210.4",
3
+ "version": "0.212.0",
4
4
  "description": "Svelte UI library",
5
5
  "tagline": "friendly user zystem",
6
6
  "glyph": "🧶",
package/src/lib/alert.ts CHANGED
@@ -4,7 +4,7 @@ export type AlertStatus = 'inform' | 'help' | 'error';
4
4
 
5
5
  export interface AlertStatusOptions {
6
6
  color: string;
7
- icon: string | null;
7
+ icon: string;
8
8
  }
9
9
 
10
10
  export const alert_status_options: Record<AlertStatus, AlertStatusOptions> = {
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Helpers for relating links to the current page.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ /** A pathname without its trailing slash, except the root's. */
8
+ const strip_trailing_slash = (pathname: string): string =>
9
+ pathname.length > 1 && pathname.endsWith('/') ? pathname.slice(0, -1) : pathname;
10
+
11
+ /**
12
+ * Whether `href` leads to the page at `url`: the same origin and pathname, with a
13
+ * trailing slash, the query, and the hash ignored. A relative `href` resolves
14
+ * against `url`, as a browser resolves it.
15
+ *
16
+ * @param href - a link's `href`, absolute or relative
17
+ * @param url - the current page's url, e.g. `page.url`
18
+ * @returns `false` for an `href` that isn't a valid url
19
+ */
20
+ export const href_is_current_page = (href: string, url: URL): boolean => {
21
+ if (!URL.canParse(href, url)) return false;
22
+ const target = new URL(href, url);
23
+ return (
24
+ target.origin === url.origin &&
25
+ strip_trailing_slash(target.pathname) === strip_trailing_slash(url.pathname)
26
+ );
27
+ };