@lyeve-labs/ui-kit 0.15.0 → 0.17.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.
@@ -21,7 +21,7 @@
21
21
  * reader announce the page name twice.
22
22
  */
23
23
  import type { Snippet } from 'svelte';
24
- import { Menu } from '@lucide/svelte';
24
+ import { Menu, PanelLeft, PanelLeftClose } from '@lucide/svelte';
25
25
  import { overlay } from '../internal/overlay.js';
26
26
  import { APP_BRAND, APP_HEADER, APP_SIDEBAR, APP_SIDEBAR_BAND } from '../internal/layout.js';
27
27
 
@@ -30,6 +30,23 @@
30
30
  section?: string;
31
31
  /** Bindable so the app router can close the drawer after a navigation. */
32
32
  navOpen?: boolean;
33
+ /**
34
+ * Offers a control that puts the sidebar away above md:.
35
+ *
36
+ * Opt-in rather than on by default: three products render this shell, and
37
+ * a new button in every header of all three is a decision each of them
38
+ * makes for itself. A page that owns the viewport - a canvas, a split pane
39
+ * - is the case it exists for.
40
+ */
41
+ collapsible?: boolean;
42
+ /**
43
+ * Whether the sidebar is put away. Desktop only: below md: the sidebar is
44
+ * already a drawer and `navOpen` is the state that governs it.
45
+ *
46
+ * Bindable and not held here, because where it is remembered is the app's
47
+ * decision. Nothing persists it for you.
48
+ */
49
+ collapsed?: boolean;
33
50
  /** The sidebar landmark's accessible name. */
34
51
  sidebarLabel?: string;
35
52
  /** The drawer's accessible name, below md: where the sidebar is a dialog. */
@@ -49,6 +66,8 @@
49
66
  let {
50
67
  section = undefined,
51
68
  navOpen = $bindable(false),
69
+ collapsible = false,
70
+ collapsed = $bindable(false),
52
71
  sidebarLabel = 'Sidebar',
53
72
  drawerLabel = 'Navigation',
54
73
  brand,
@@ -66,6 +85,11 @@
66
85
  */
67
86
  let isMobile = $state(false);
68
87
  const drawerOpen = $derived(isMobile && navOpen);
88
+ /**
89
+ * Collapsing is a desktop gesture. Below md: the same aside is the drawer,
90
+ * so honouring `collapsed` there would leave the hamburger opening nothing.
91
+ */
92
+ const railHidden = $derived(collapsible && collapsed && !isMobile);
69
93
 
70
94
  $effect(() => {
71
95
  const mq = window.matchMedia('(max-width: 767px)');
@@ -129,7 +153,7 @@
129
153
  >
130
154
  {@render sidebar(false)}
131
155
  </div>
132
- {:else}
156
+ {:else if !railHidden}
133
157
  <div class="hidden md:flex">
134
158
  {@render sidebar(isMobile)}
135
159
  </div>
@@ -151,6 +175,19 @@
151
175
  >
152
176
  <Menu size={20} />
153
177
  </button>
178
+ {:else if collapsible}
179
+ <!-- Same square and the same gutter as the hamburger it replaces, so
180
+ the header's first control sits in one place at every width. -->
181
+ <button
182
+ type="button"
183
+ data-testid="app-sidebar-toggle"
184
+ class="-ms-2 flex h-11 w-11 shrink-0 items-center justify-center rounded-lg text-muted outline-none transition-colors duration-150 hover:bg-surface-2 hover:text-fg focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
185
+ aria-label={collapsed ? 'Show sidebar' : 'Hide sidebar'}
186
+ aria-expanded={!collapsed}
187
+ onclick={() => (collapsed = !collapsed)}
188
+ >
189
+ {#if collapsed}<PanelLeft size={18} />{:else}<PanelLeftClose size={18} />{/if}
190
+ </button>
154
191
  {/if}
155
192
  {#if section}
156
193
  <span data-testid="app-section" class="truncate text-sm font-semibold text-fg">
@@ -25,6 +25,23 @@ interface Props {
25
25
  section?: string;
26
26
  /** Bindable so the app router can close the drawer after a navigation. */
27
27
  navOpen?: boolean;
28
+ /**
29
+ * Offers a control that puts the sidebar away above md:.
30
+ *
31
+ * Opt-in rather than on by default: three products render this shell, and
32
+ * a new button in every header of all three is a decision each of them
33
+ * makes for itself. A page that owns the viewport - a canvas, a split pane
34
+ * - is the case it exists for.
35
+ */
36
+ collapsible?: boolean;
37
+ /**
38
+ * Whether the sidebar is put away. Desktop only: below md: the sidebar is
39
+ * already a drawer and `navOpen` is the state that governs it.
40
+ *
41
+ * Bindable and not held here, because where it is remembered is the app's
42
+ * decision. Nothing persists it for you.
43
+ */
44
+ collapsed?: boolean;
28
45
  /** The sidebar landmark's accessible name. */
29
46
  sidebarLabel?: string;
30
47
  /** The drawer's accessible name, below md: where the sidebar is a dialog. */
@@ -40,6 +57,6 @@ interface Props {
40
57
  class?: string;
41
58
  children: Snippet;
42
59
  }
43
- declare const AppShell: import("svelte").Component<Props, {}, "navOpen">;
60
+ declare const AppShell: import("svelte").Component<Props, {}, "navOpen" | "collapsed">;
44
61
  type AppShell = ReturnType<typeof AppShell>;
45
62
  export default AppShell;
@@ -72,9 +72,7 @@
72
72
  aria-label={title ? undefined : 'Dialog'}
73
73
  >
74
74
  {#if title}
75
- <div
76
- class="flex shrink-0 items-start justify-between gap-4 px-5 py-4 border-b border-line"
77
- >
75
+ <div class="flex shrink-0 items-start justify-between gap-4 px-5 py-4 border-b border-line">
78
76
  <div>
79
77
  <h2 id={headingId} class="font-semibold text-fg">{title}</h2>
80
78
  {#if description}
@@ -10,17 +10,23 @@
10
10
  * the element, and the element is always a real heading.
11
11
  */
12
12
  import type { Snippet } from 'svelte';
13
- import { sectionHeading } from '../internal/layout.js';
13
+ import { sectionHeading, type SectionVariant } from '../internal/layout.js';
14
14
 
15
15
  interface Props {
16
- /** 2 under a page title, 3 inside a card. */
16
+ /** 2 under a page title, 3 inside a card. The element, not the treatment. */
17
17
  level?: 2 | 3;
18
+ /**
19
+ * `eyebrow` is the small uppercase label the consoles head a band with. It
20
+ * is separate from the level because an eyebrow appears at both, and it was
21
+ * unnamed here while thirty of them shipped hand rolled across two apps.
22
+ */
23
+ variant?: SectionVariant;
18
24
  actions?: Snippet;
19
25
  class?: string;
20
26
  children: Snippet;
21
27
  }
22
28
 
23
- let { level = 2, actions, class: klass = '', children }: Props = $props();
29
+ let { level = 2, variant = 'default', actions, class: klass = '', children }: Props = $props();
24
30
  </script>
25
31
 
26
32
  <div class="flex flex-wrap items-center justify-between gap-4 {klass}">
@@ -28,9 +34,9 @@
28
34
  structure decision, and a reader of this file should be able to see both
29
35
  headings it can produce. -->
30
36
  {#if level === 2}
31
- <h2 class={sectionHeading(2)}>{@render children()}</h2>
37
+ <h2 class={sectionHeading(2, variant)}>{@render children()}</h2>
32
38
  {:else}
33
- <h3 class={sectionHeading(3)}>{@render children()}</h3>
39
+ <h3 class={sectionHeading(3, variant)}>{@render children()}</h3>
34
40
  {/if}
35
41
 
36
42
  {#if actions}
@@ -9,9 +9,16 @@
9
9
  * the element, and the element is always a real heading.
10
10
  */
11
11
  import type { Snippet } from 'svelte';
12
+ import { type SectionVariant } from '../internal/layout.js';
12
13
  interface Props {
13
- /** 2 under a page title, 3 inside a card. */
14
+ /** 2 under a page title, 3 inside a card. The element, not the treatment. */
14
15
  level?: 2 | 3;
16
+ /**
17
+ * `eyebrow` is the small uppercase label the consoles head a band with. It
18
+ * is separate from the level because an eyebrow appears at both, and it was
19
+ * unnamed here while thirty of them shipped hand rolled across two apps.
20
+ */
21
+ variant?: SectionVariant;
15
22
  actions?: Snippet;
16
23
  class?: string;
17
24
  children: Snippet;
@@ -14,10 +14,7 @@
14
14
  import type { DialogEntry } from './types.js';
15
15
  import { sizeClass } from './types.js';
16
16
  import { overlay } from '../../internal/overlay.js';
17
- import {
18
- closeDialog,
19
- dismissDialog,
20
- } from './dialog-manager.svelte';
17
+ import { closeDialog, dismissDialog } from './dialog-manager.svelte';
21
18
  import ConfirmDialog from './ConfirmDialog.svelte';
22
19
 
23
20
  let {
package/dist/index.d.ts CHANGED
@@ -20,7 +20,7 @@ export { default as Table } from './components/Table.svelte';
20
20
  export { default as DescriptionList } from './components/DescriptionList.svelte';
21
21
  export { default as Toolbar } from './components/Toolbar.svelte';
22
22
  export { default as TreeView } from './components/TreeView.svelte';
23
- export type { PageWidth } from './internal/layout.js';
23
+ export type { PageWidth, SectionVariant } from './internal/layout.js';
24
24
  export type { TreeNode, TriState } from './internal/tree.js';
25
25
  export { default as Button } from './components/Button.svelte';
26
26
  export { default as ButtonGroup } from './components/ButtonGroup.svelte';
@@ -86,4 +86,4 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
86
86
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
87
87
  export { cn, type ClassValue } from './utils/cn.js';
88
88
  export { getTheme, setTheme, toggleTheme, themeBootScript, type Theme } from './utils/theme.js';
89
- export declare const VERSION = "0.15.0";
89
+ export declare const VERSION = "0.17.0";
package/dist/index.js CHANGED
@@ -92,4 +92,4 @@ export { getTheme, setTheme, toggleTheme, themeBootScript } from './utils/theme.
92
92
  // ── Version ────────────────────────────────────────────────────────────────
93
93
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
94
94
  // this line; the build and the test suite fail when the two disagree.
95
- export const VERSION = '0.15.0';
95
+ export const VERSION = '0.17.0';
@@ -17,6 +17,8 @@
17
17
  *
18
18
  * Not exported from the package entry point - this is an implementation detail.
19
19
  */
20
+ /** The treatment a section heading takes, independent of its level. */
21
+ export type SectionVariant = 'default' | 'eyebrow';
20
22
  /** How much of the viewport a page's content is allowed to fill. */
21
23
  export type PageWidth = 'narrow' | 'default' | 'wide' | 'full';
22
24
  /**
@@ -115,8 +117,16 @@ export declare const MODAL_PAD = "px-card py-card-sm";
115
117
  * page can render at different sizes and weights. Taking the level rather than
116
118
  * a free-form string means the class cannot disagree with the heading element
117
119
  * the caller is already writing.
120
+ *
121
+ * `eyebrow` is the small uppercase label the consoles use to head a band of
122
+ * content. It is a second treatment rather than a third level because the
123
+ * element is a decision about document structure and the treatment is not: an
124
+ * eyebrow appears at both levels. It went unnamed here while thirty of them
125
+ * shipped hand rolled across two apps in four different bottom margins, and a
126
+ * margin is exactly what this function must not carry - the stack around the
127
+ * heading owns the distance to what follows.
118
128
  */
119
- export declare function sectionHeading(level: 2 | 3): string;
129
+ export declare function sectionHeading(level: 2 | 3, variant?: SectionVariant): string;
120
130
  /**
121
131
  * The authed app frame, stated once for every app that has one.
122
132
  *
@@ -123,8 +123,20 @@ export const MODAL_PAD = 'px-card py-card-sm';
123
123
  * page can render at different sizes and weights. Taking the level rather than
124
124
  * a free-form string means the class cannot disagree with the heading element
125
125
  * the caller is already writing.
126
+ *
127
+ * `eyebrow` is the small uppercase label the consoles use to head a band of
128
+ * content. It is a second treatment rather than a third level because the
129
+ * element is a decision about document structure and the treatment is not: an
130
+ * eyebrow appears at both levels. It went unnamed here while thirty of them
131
+ * shipped hand rolled across two apps in four different bottom margins, and a
132
+ * margin is exactly what this function must not carry - the stack around the
133
+ * heading owns the distance to what follows.
126
134
  */
127
- export function sectionHeading(level) {
135
+ export function sectionHeading(level, variant = 'default') {
136
+ // The eyebrow reads the same at both levels. It is a label for the band under
137
+ // it rather than a title, so it does not take the level's size at all.
138
+ if (variant === 'eyebrow')
139
+ return 'text-xs font-medium uppercase tracking-wide text-faint';
128
140
  // Level 3 drops a size rather than a weight. Inside a card it sits under the
129
141
  // card's own semibold title, and two semibold lines at the same size read as
130
142
  // one heading broken in half.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lyeve-labs/ui-kit",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "A clean, accessible, palette-aware Svelte 5 component library. The design system behind LyEve.",
5
5
  "license": "MIT",
6
6
  "author": "LyEve Labs <hello@lyeve.com>",