@classic-homes/theme-svelte 1.34.1 → 1.35.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.
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { IconName } from './Icon.svelte';
|
|
3
|
+
|
|
4
|
+
export interface AppLauncherItem {
|
|
5
|
+
id: string;
|
|
6
|
+
/** Name under the tile */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Where the tile goes: a real link, so Ctrl/middle-click open a new tab */
|
|
9
|
+
href: string;
|
|
10
|
+
/** Tile icon (ignored when the `icon` snippet is set) */
|
|
11
|
+
icon?: IconName;
|
|
12
|
+
/** Opens in a new tab (`target="_blank"`, `rel="noopener noreferrer"`) */
|
|
13
|
+
external?: boolean;
|
|
14
|
+
/** Shown as the tile's tooltip */
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<script lang="ts">
|
|
20
|
+
import { Popover } from 'bits-ui';
|
|
21
|
+
import type { Snippet } from 'svelte';
|
|
22
|
+
import { cn } from '../utils.js';
|
|
23
|
+
import Icon from './Icon.svelte';
|
|
24
|
+
import { buttonVariants } from './button-variants.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AppLauncher - the header's "apps" grid: a grid-icon button that opens a
|
|
28
|
+
* panel of tiles linking to the other systems people use.
|
|
29
|
+
*
|
|
30
|
+
* Tiles are plain links, so Ctrl/middle-click open a new tab; external ones
|
|
31
|
+
* open in a new tab anyway. Arrow keys move between tiles, Home/End jump to
|
|
32
|
+
* the first/last, Escape closes and returns focus to the button.
|
|
33
|
+
*/
|
|
34
|
+
interface Props {
|
|
35
|
+
apps: AppLauncherItem[];
|
|
36
|
+
/** Accessible name of the button and the panel (default: "Apps") */
|
|
37
|
+
triggerLabel?: string;
|
|
38
|
+
/** Heading at the top of the panel; none when unset */
|
|
39
|
+
title?: string;
|
|
40
|
+
/** Tiles per row (default: 3) */
|
|
41
|
+
columns?: number;
|
|
42
|
+
/** Panel alignment against the button (default: end) */
|
|
43
|
+
align?: 'start' | 'center' | 'end';
|
|
44
|
+
/** Panel open (bindable) */
|
|
45
|
+
open?: boolean;
|
|
46
|
+
onOpenChange?: (open: boolean) => void;
|
|
47
|
+
/** Classes for the panel */
|
|
48
|
+
class?: string;
|
|
49
|
+
/** Classes for the trigger button */
|
|
50
|
+
triggerClass?: string;
|
|
51
|
+
/** Draws a tile's icon instead of `item.icon`, e.g. an app's own mark */
|
|
52
|
+
icon?: Snippet<[AppLauncherItem]>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let {
|
|
56
|
+
apps,
|
|
57
|
+
triggerLabel = 'Apps',
|
|
58
|
+
title,
|
|
59
|
+
columns = 3,
|
|
60
|
+
align = 'end',
|
|
61
|
+
open = $bindable(false),
|
|
62
|
+
onOpenChange,
|
|
63
|
+
class: className,
|
|
64
|
+
triggerClass,
|
|
65
|
+
icon,
|
|
66
|
+
}: Props = $props();
|
|
67
|
+
|
|
68
|
+
function handleOpenChange(next: boolean) {
|
|
69
|
+
open = next;
|
|
70
|
+
onOpenChange?.(next);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Arrow keys walk the grid by row and column; Home/End go to the ends. */
|
|
74
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
75
|
+
const tiles = Array.from(
|
|
76
|
+
(event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[data-app-launcher-tile]')
|
|
77
|
+
);
|
|
78
|
+
const current = tiles.indexOf(document.activeElement as HTMLElement);
|
|
79
|
+
if (current === -1) return;
|
|
80
|
+
const last = tiles.length - 1;
|
|
81
|
+
const step: Record<string, number> = {
|
|
82
|
+
ArrowRight: 1,
|
|
83
|
+
ArrowLeft: -1,
|
|
84
|
+
ArrowDown: columns,
|
|
85
|
+
ArrowUp: -columns,
|
|
86
|
+
};
|
|
87
|
+
let next: number;
|
|
88
|
+
if (event.key === 'Home') next = 0;
|
|
89
|
+
else if (event.key === 'End') next = last;
|
|
90
|
+
else if (event.key in step) next = current + step[event.key];
|
|
91
|
+
else return;
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
tiles[Math.min(Math.max(next, 0), last)].focus();
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<Popover.Root bind:open onOpenChange={handleOpenChange}>
|
|
98
|
+
<Popover.Trigger
|
|
99
|
+
class={cn(buttonVariants({ variant: 'ghost', size: 'icon' }), triggerClass)}
|
|
100
|
+
aria-label={triggerLabel}
|
|
101
|
+
>
|
|
102
|
+
<Icon name="grid" size={20} aria-hidden="true" />
|
|
103
|
+
</Popover.Trigger>
|
|
104
|
+
<Popover.Portal>
|
|
105
|
+
<Popover.Content
|
|
106
|
+
{align}
|
|
107
|
+
sideOffset={6}
|
|
108
|
+
collisionPadding={8}
|
|
109
|
+
class={cn(
|
|
110
|
+
'z-50 max-h-[calc(100dvh-5rem)] w-[min(20rem,calc(100vw-2rem))] overflow-y-auto rounded-lg border border-border bg-popover p-2 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
111
|
+
className
|
|
112
|
+
)}
|
|
113
|
+
>
|
|
114
|
+
<nav aria-label={title ?? triggerLabel}>
|
|
115
|
+
{#if title}
|
|
116
|
+
<p class="px-2 pb-2 pt-1 text-sm font-semibold">{title}</p>
|
|
117
|
+
{/if}
|
|
118
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
119
|
+
<ul
|
|
120
|
+
class="grid gap-1"
|
|
121
|
+
style:grid-template-columns="repeat({columns}, minmax(0, 1fr))"
|
|
122
|
+
onkeydown={handleKeydown}
|
|
123
|
+
>
|
|
124
|
+
{#each apps as app (app.id)}
|
|
125
|
+
<li>
|
|
126
|
+
<a
|
|
127
|
+
href={app.href}
|
|
128
|
+
target={app.external ? '_blank' : undefined}
|
|
129
|
+
rel={app.external ? 'noopener noreferrer' : undefined}
|
|
130
|
+
title={app.description}
|
|
131
|
+
data-app-launcher-tile={app.id}
|
|
132
|
+
class="flex h-full flex-col items-center gap-1.5 rounded-md px-1 py-3 text-center text-xs font-medium text-foreground hover:bg-muted-hover focus-visible:bg-muted-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
133
|
+
onclick={() => handleOpenChange(false)}
|
|
134
|
+
>
|
|
135
|
+
<span
|
|
136
|
+
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary"
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
>
|
|
139
|
+
{#if icon}
|
|
140
|
+
{@render icon(app)}
|
|
141
|
+
{:else}
|
|
142
|
+
<Icon name={app.icon ?? 'external-link'} size={22} />
|
|
143
|
+
{/if}
|
|
144
|
+
</span>
|
|
145
|
+
<span class="line-clamp-2 break-words">{app.label}</span>
|
|
146
|
+
{#if app.external}
|
|
147
|
+
<span class="sr-only">(opens in a new tab)</span>
|
|
148
|
+
{/if}
|
|
149
|
+
</a>
|
|
150
|
+
</li>
|
|
151
|
+
{/each}
|
|
152
|
+
</ul>
|
|
153
|
+
</nav>
|
|
154
|
+
</Popover.Content>
|
|
155
|
+
</Popover.Portal>
|
|
156
|
+
</Popover.Root>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { IconName } from './Icon.svelte';
|
|
2
|
+
export interface AppLauncherItem {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Name under the tile */
|
|
5
|
+
label: string;
|
|
6
|
+
/** Where the tile goes: a real link, so Ctrl/middle-click open a new tab */
|
|
7
|
+
href: string;
|
|
8
|
+
/** Tile icon (ignored when the `icon` snippet is set) */
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
/** Opens in a new tab (`target="_blank"`, `rel="noopener noreferrer"`) */
|
|
11
|
+
external?: boolean;
|
|
12
|
+
/** Shown as the tile's tooltip */
|
|
13
|
+
description?: string;
|
|
14
|
+
}
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
/**
|
|
17
|
+
* AppLauncher - the header's "apps" grid: a grid-icon button that opens a
|
|
18
|
+
* panel of tiles linking to the other systems people use.
|
|
19
|
+
*
|
|
20
|
+
* Tiles are plain links, so Ctrl/middle-click open a new tab; external ones
|
|
21
|
+
* open in a new tab anyway. Arrow keys move between tiles, Home/End jump to
|
|
22
|
+
* the first/last, Escape closes and returns focus to the button.
|
|
23
|
+
*/
|
|
24
|
+
interface Props {
|
|
25
|
+
apps: AppLauncherItem[];
|
|
26
|
+
/** Accessible name of the button and the panel (default: "Apps") */
|
|
27
|
+
triggerLabel?: string;
|
|
28
|
+
/** Heading at the top of the panel; none when unset */
|
|
29
|
+
title?: string;
|
|
30
|
+
/** Tiles per row (default: 3) */
|
|
31
|
+
columns?: number;
|
|
32
|
+
/** Panel alignment against the button (default: end) */
|
|
33
|
+
align?: 'start' | 'center' | 'end';
|
|
34
|
+
/** Panel open (bindable) */
|
|
35
|
+
open?: boolean;
|
|
36
|
+
onOpenChange?: (open: boolean) => void;
|
|
37
|
+
/** Classes for the panel */
|
|
38
|
+
class?: string;
|
|
39
|
+
/** Classes for the trigger button */
|
|
40
|
+
triggerClass?: string;
|
|
41
|
+
/** Draws a tile's icon instead of `item.icon`, e.g. an app's own mark */
|
|
42
|
+
icon?: Snippet<[AppLauncherItem]>;
|
|
43
|
+
}
|
|
44
|
+
declare const AppLauncher: import("svelte").Component<Props, {}, "open">;
|
|
45
|
+
type AppLauncher = ReturnType<typeof AppLauncher>;
|
|
46
|
+
export default AppLauncher;
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export { default as Dialog } from './components/Dialog.svelte';
|
|
|
41
41
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
42
42
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
43
43
|
export { default as DropdownMenu } from './components/DropdownMenu.svelte';
|
|
44
|
+
export { default as AppLauncher, type AppLauncherItem } from './components/AppLauncher.svelte';
|
|
44
45
|
export { default as DataPanel } from './components/DataPanel.svelte';
|
|
45
46
|
export { default as DataPanelHeader } from './components/DataPanelHeader.svelte';
|
|
46
47
|
export { default as DataPanelContent } from './components/DataPanelContent.svelte';
|
package/dist/lib/index.js
CHANGED
|
@@ -49,6 +49,7 @@ export { default as Dialog } from './components/Dialog.svelte';
|
|
|
49
49
|
export { default as AlertDialog } from './components/AlertDialog.svelte';
|
|
50
50
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
51
51
|
export { default as DropdownMenu } from './components/DropdownMenu.svelte';
|
|
52
|
+
export { default as AppLauncher } from './components/AppLauncher.svelte';
|
|
52
53
|
// DataPanel Components
|
|
53
54
|
export { default as DataPanel } from './components/DataPanel.svelte';
|
|
54
55
|
export { default as DataPanelHeader } from './components/DataPanelHeader.svelte';
|