@placeholderco/placeholder-ui 2.2.2 → 2.2.3

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.
@@ -12,10 +12,16 @@
12
12
  import Logo from '../ui/Logo.svelte';
13
13
  import type { Snippet } from 'svelte';
14
14
 
15
- export type NavbarVariant = 'default' | 'primary';
15
+ export type NavbarVariant = 'default' | 'primary' | 'site';
16
16
 
17
17
  export interface NavbarProps {
18
- /** Visual variant. `primary` uses the primary brand colour as the background across all themes */
18
+ /**
19
+ * Visual variant.
20
+ * - `primary` uses the primary brand colour as the background across all themes.
21
+ * - `site` is a marketing-site header: logo on the left, links pushed to the right with an
22
+ * accent underline on the `active` item. Below 768px the links collapse and the drawer
23
+ * button (enable with `showDrawerButton`) is shown instead.
24
+ */
19
25
  variant?: NavbarVariant;
20
26
  /** App navigation link displayed as a header */
21
27
  appNav?: Hyperlink;
@@ -67,10 +73,16 @@
67
73
  }: NavbarProps = $props();
68
74
 
69
75
  const isPrimary = $derived(variant === 'primary');
76
+ const isSite = $derived(variant === 'site');
70
77
  </script>
71
78
 
72
- <header class={isPrimary ? 'primary' : ''}>
79
+ <header class={isPrimary ? 'primary' : isSite ? 'site' : ''}>
73
80
  <div class="inner-navbar {className} {inContainer ? 'container' : ''}">
81
+ {#if isSite && !noLogo}
82
+ <span class="site-logo">
83
+ <Logo href={logoHref} />
84
+ </span>
85
+ {/if}
74
86
  {@render leftSection?.()}
75
87
  {#if showDrawerButton && drawerButtonPosition === 'left'}
76
88
  <ActionIcon
@@ -122,7 +134,7 @@
122
134
  darkVariant="accent-subtle"
123
135
  lightVariant={isPrimary ? 'accent-subtle' : 'primary-subtle'}
124
136
  />
125
- {#if !viewportState.isMobile && !noLogo}
137
+ {#if !isSite && !viewportState.isMobile && !noLogo}
126
138
  {#if logoHref}
127
139
  <Logo href={logoHref} fill={isPrimary ? 'white' : undefined} />
128
140
  {:else}
@@ -197,6 +209,112 @@
197
209
  color: var(--ui-accent);
198
210
  }
199
211
 
212
+ /* Site variant: marketing-site header. Logo on the left, links on the right with a 2px
213
+ underline that lights up (accent) on the active item. */
214
+ header.site .inner-navbar .links-container {
215
+ gap: 1.5rem;
216
+ }
217
+
218
+ header.site .inner-navbar .links-container > :global(a),
219
+ header.site
220
+ .inner-navbar
221
+ .links-container
222
+ > :global(.navbar-parent)
223
+ > :global(.navbar-parent-trigger) {
224
+ font-weight: 500;
225
+ padding: 0.35rem 0;
226
+ border-bottom: 2px solid transparent;
227
+ transition:
228
+ color 200ms,
229
+ border-color 200ms;
230
+ }
231
+
232
+ header.site .inner-navbar .links-container > :global(a:hover),
233
+ header.site
234
+ .inner-navbar
235
+ .links-container
236
+ > :global(.navbar-parent)
237
+ > :global(.navbar-parent-trigger:hover) {
238
+ color: var(--ui-tertiary-dark);
239
+ }
240
+
241
+ :global(.dark) header.site .inner-navbar .links-container > :global(a:hover),
242
+ :global(.dark)
243
+ header.site
244
+ .inner-navbar
245
+ .links-container
246
+ > :global(.navbar-parent)
247
+ > :global(.navbar-parent-trigger:hover) {
248
+ color: var(--ui-accent);
249
+ }
250
+
251
+ header.site .inner-navbar .links-container > :global(a.active),
252
+ header.site
253
+ .inner-navbar
254
+ .links-container
255
+ > :global(.navbar-parent)
256
+ > :global(.navbar-parent-trigger.active) {
257
+ border-bottom-color: var(--ui-accent);
258
+ }
259
+
260
+ /* Brand logo sits on the left and is sized by height so any aspect ratio works */
261
+ header.site .site-logo {
262
+ display: inline-flex;
263
+ align-items: center;
264
+ flex-shrink: 0;
265
+ }
266
+
267
+ header.site .site-logo :global(.logo),
268
+ header.site .site-logo :global(.pui-icon) {
269
+ display: inline-flex;
270
+ align-items: center;
271
+ }
272
+
273
+ header.site .site-logo :global(.pui-icon svg) {
274
+ height: 40px;
275
+ width: auto;
276
+ display: block;
277
+ }
278
+
279
+ /* Primary links are right-aligned in the site variant (unless a growing .middle exists) */
280
+ header.site .primary.links-container {
281
+ margin-left: auto;
282
+ }
283
+
284
+ header.site .primary.links-container ~ .secondary,
285
+ header.site .primary.links-container ~ .right,
286
+ header.site .primary.links-container ~ :global(.theme-switcher) {
287
+ margin-left: 0;
288
+ }
289
+
290
+ @media (max-width: 767px) {
291
+ /* Links collapse into the drawer; the drawer button takes their place */
292
+ header.site .inner-navbar .links-container {
293
+ display: none;
294
+ }
295
+
296
+ header.site .site-logo :global(.pui-icon svg) {
297
+ height: 34px;
298
+ }
299
+
300
+ /* The hidden links containers still count as siblings, so re-apply the auto margin
301
+ to the first visible right-hand element */
302
+ header.site .inner-navbar > .right,
303
+ header.site .inner-navbar > :global(.theme-switcher) {
304
+ margin-left: auto;
305
+ }
306
+
307
+ header.site .inner-navbar > .right ~ :global(.theme-switcher) {
308
+ margin-left: 0;
309
+ }
310
+ }
311
+
312
+ @media (min-width: 768px) {
313
+ header.site .inner-navbar :global(.drawer-btn) {
314
+ display: none;
315
+ }
316
+ }
317
+
200
318
  .inner-navbar {
201
319
  display: flex;
202
320
  align-items: center;
@@ -1,9 +1,15 @@
1
1
  import type { NavbarItem } from '../models/NavbarItem.js';
2
2
  import { type Hyperlink } from '../index.js';
3
3
  import type { Snippet } from 'svelte';
4
- export type NavbarVariant = 'default' | 'primary';
4
+ export type NavbarVariant = 'default' | 'primary' | 'site';
5
5
  export interface NavbarProps {
6
- /** Visual variant. `primary` uses the primary brand colour as the background across all themes */
6
+ /**
7
+ * Visual variant.
8
+ * - `primary` uses the primary brand colour as the background across all themes.
9
+ * - `site` is a marketing-site header: logo on the left, links pushed to the right with an
10
+ * accent underline on the `active` item. Below 768px the links collapse and the drawer
11
+ * button (enable with `showDrawerButton`) is shown instead.
12
+ */
7
13
  variant?: NavbarVariant;
8
14
  /** App navigation link displayed as a header */
9
15
  appNav?: Hyperlink;
@@ -25,7 +25,12 @@
25
25
  {@const Component = item.component}
26
26
  <Component {...item.componentProps} />
27
27
  {:else if 'href' in item}
28
- <Link href={item.href} onclick={item.onclick} class="navbar-item {item.class ?? ''}">
28
+ <Link
29
+ href={item.href}
30
+ onclick={item.onclick}
31
+ class="navbar-item {item.class ?? ''} {item.active ? 'active' : ''}"
32
+ ariaCurrent={item.active ? 'page' : undefined}
33
+ >
29
34
  {#if item.iconSvg}
30
35
  <Icon svg={item.iconSvg} size="1.1em" class="navbar-item-icon" />
31
36
  {/if}
@@ -35,7 +40,7 @@
35
40
  <div class="navbar-parent" use:clickOutside={close}>
36
41
  <button
37
42
  type="button"
38
- class="navbar-item navbar-parent-trigger {item.class ?? ''}"
43
+ class="navbar-item navbar-parent-trigger {item.class ?? ''} {item.active ? 'active' : ''}"
39
44
  aria-expanded={open}
40
45
  aria-haspopup="true"
41
46
  onclick={(e) => {
@@ -3,6 +3,8 @@ type BaseNavbarItem = {
3
3
  alwaysShow?: boolean;
4
4
  showInOnly?: undefined | 'Navbar' | 'Drawer';
5
5
  class?: string;
6
+ /** Marks the item as the current page. Adds an `active` class (and `aria-current="page"` on links); the `site` navbar variant underlines it. */
7
+ active?: boolean;
6
8
  /** Custom click handler. For link items, call event.preventDefault() to override the default href navigation. */
7
9
  onclick?: (event: MouseEvent) => void;
8
10
  };
@@ -16,6 +16,8 @@
16
16
  children?: Snippet;
17
17
  /** Click event handler */
18
18
  onclick?: (event: MouseEvent) => void;
19
+ /** Value for the `aria-current` attribute (e.g. `'page'` for the current page) */
20
+ ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | undefined;
19
21
  }
20
22
 
21
23
  let {
@@ -24,7 +26,8 @@
24
26
  target = undefined,
25
27
  class: classes = '',
26
28
  children,
27
- onclick
29
+ onclick,
30
+ ariaCurrent = undefined
28
31
  }: LinkProps = $props();
29
32
  </script>
30
33
 
@@ -33,6 +36,7 @@
33
36
  class={classes}
34
37
  href={href?.startsWith('/') ? resolve(href as any) : href}
35
38
  {rel}
39
+ aria-current={ariaCurrent}
36
40
  onclick={(e) => interceptLinkClick(e, { href, target, onclick })}
37
41
  data-sveltekit-preload-data="hover"
38
42
  >
@@ -12,6 +12,8 @@ export interface LinkProps {
12
12
  children?: Snippet;
13
13
  /** Click event handler */
14
14
  onclick?: (event: MouseEvent) => void;
15
+ /** Value for the `aria-current` attribute (e.g. `'page'` for the current page) */
16
+ ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | undefined;
15
17
  }
16
18
  declare const Link: import("svelte").Component<LinkProps, {}, "">;
17
19
  type Link = ReturnType<typeof Link>;
@@ -84,6 +84,6 @@ export interface TableProps<T = any> {
84
84
  * Use with `onsort` when the server returns pre-sorted rows. */
85
85
  manualSort?: boolean;
86
86
  }
87
- declare const Table: import("svelte").Component<TableProps<any>, {}, "selectedRows" | "expandedKeys" | "page" | "sortKey" | "sortDirection">;
87
+ declare const Table: import("svelte").Component<TableProps<any>, {}, "page" | "selectedRows" | "expandedKeys" | "sortKey" | "sortDirection">;
88
88
  type Table = ReturnType<typeof Table>;
89
89
  export default Table;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@placeholderco/placeholder-ui",
3
- "version": "2.2.2",
3
+ "version": "2.2.3",
4
4
  "description": "A modern, customizable Svelte 5 UI component library with comprehensive theming support. Re-brand it with a single config object.",
5
5
  "license": "MIT",
6
6
  "author": "Placeholderco",