@atmo-dev/kit 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +9 -2
- package/dist/components/Avatar.svelte +35 -0
- package/dist/components/Avatar.svelte.d.ts +9 -0
- package/dist/components/Badge.svelte +21 -0
- package/dist/components/Badge.svelte.d.ts +7 -0
- package/dist/components/Button.svelte +94 -0
- package/dist/components/Button.svelte.d.ts +13 -0
- package/dist/components/Checkbox.svelte +29 -0
- package/dist/components/Checkbox.svelte.d.ts +8 -0
- package/dist/components/Heading.svelte +24 -0
- package/dist/components/Heading.svelte.d.ts +8 -0
- package/dist/components/Icon.svelte +24 -0
- package/dist/components/Icon.svelte.d.ts +8 -0
- package/dist/components/IconButton.svelte +24 -0
- package/dist/components/IconButton.svelte.d.ts +8 -0
- package/dist/components/IconRail.svelte +183 -0
- package/dist/components/IconRail.svelte.d.ts +26 -0
- package/dist/components/InlineEditor.svelte +120 -0
- package/dist/components/InlineEditor.svelte.d.ts +19 -0
- package/dist/components/Input.svelte +32 -0
- package/dist/components/Input.svelte.d.ts +9 -0
- package/dist/components/InsetImage.svelte +29 -0
- package/dist/components/InsetImage.svelte.d.ts +9 -0
- package/dist/components/Label.svelte +19 -0
- package/dist/components/Label.svelte.d.ts +7 -0
- package/dist/components/LinkCard.svelte +47 -0
- package/dist/components/LinkCard.svelte.d.ts +12 -0
- package/dist/components/Modal.svelte +93 -0
- package/dist/components/Modal.svelte.d.ts +12 -0
- package/dist/components/NestedComments.svelte +61 -0
- package/dist/components/NestedComments.svelte.d.ts +8 -0
- package/dist/components/PageTitle.svelte +22 -0
- package/dist/components/PageTitle.svelte.d.ts +8 -0
- package/dist/components/Popover.svelte +49 -0
- package/dist/components/Popover.svelte.d.ts +12 -0
- package/dist/components/PostComposer.svelte +41 -0
- package/dist/components/PostComposer.svelte.d.ts +16 -0
- package/dist/components/PostContent.svelte +62 -0
- package/dist/components/PostContent.svelte.d.ts +8 -0
- package/dist/components/PostVideo.svelte +74 -0
- package/dist/components/PostVideo.svelte.d.ts +9 -0
- package/dist/components/Select.svelte +34 -0
- package/dist/components/Select.svelte.d.ts +9 -0
- package/dist/components/Subheading.svelte +22 -0
- package/dist/components/Subheading.svelte.d.ts +8 -0
- package/dist/components/Surface.svelte +40 -0
- package/dist/components/Surface.svelte.d.ts +11 -0
- package/dist/components/Tabs.svelte +47 -0
- package/dist/components/Tabs.svelte.d.ts +16 -0
- package/dist/components/Textarea.svelte +20 -0
- package/dist/components/Textarea.svelte.d.ts +7 -0
- package/dist/components/ToggleGroup.svelte +32 -0
- package/dist/components/ToggleGroup.svelte.d.ts +12 -0
- package/dist/components/ToggleGroupItem.svelte +26 -0
- package/dist/components/ToggleGroupItem.svelte.d.ts +8 -0
- package/dist/components/WorkspaceSidebar.svelte +82 -0
- package/dist/components/WorkspaceSidebar.svelte.d.ts +10 -0
- package/dist/components/login/ActorSearchField.svelte +238 -0
- package/dist/components/login/ActorSearchField.svelte.d.ts +19 -0
- package/dist/components/login/AtmosphereApps.svelte +19 -0
- package/dist/components/login/AtmosphereApps.svelte.d.ts +18 -0
- package/dist/components/login/LoginDialog.svelte +78 -0
- package/dist/components/login/LoginDialog.svelte.d.ts +9 -0
- package/dist/components/login/LoginForm.svelte +402 -0
- package/dist/components/login/LoginForm.svelte.d.ts +28 -0
- package/dist/components/login/RecentAccounts.svelte +57 -0
- package/dist/components/login/RecentAccounts.svelte.d.ts +10 -0
- package/dist/components/login/brands/README.md +7 -0
- package/dist/components/login/brands/blacksky.svg +3 -0
- package/dist/components/login/brands/bluesky.svg +3 -0
- package/dist/components/login/brands/bookhive.svg +3 -0
- package/dist/components/login/brands/graze.svg +3 -0
- package/dist/components/login/brands/leaflet.svg +3 -0
- package/dist/components/login/brands/pckt.svg +3 -0
- package/dist/components/login/brands/surf.svg +3 -0
- package/dist/components/login/brands/tangled.svg +3 -0
- package/dist/components/login/handle.d.ts +3 -0
- package/dist/components/login/handle.js +11 -0
- package/dist/components/login/permissions.d.ts +3 -0
- package/dist/components/login/permissions.js +13 -0
- package/dist/components/login/recent-accounts.d.ts +16 -0
- package/dist/components/login/recent-accounts.js +87 -0
- package/dist/components/login/search.d.ts +9 -0
- package/dist/components/login/search.js +47 -0
- package/dist/components/login/types.d.ts +27 -0
- package/dist/components/login/types.js +1 -0
- package/dist/confetti.d.ts +2 -0
- package/dist/confetti.js +13 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +40 -0
- package/dist/palettes.d.ts +2 -0
- package/dist/palettes.js +30 -0
- package/dist/social/mention-suggestions.d.ts +35 -0
- package/dist/social/mention-suggestions.js +122 -0
- package/dist/social/posts.d.ts +95 -0
- package/dist/social/posts.js +59 -0
- package/dist/social/richtext.d.ts +25 -0
- package/dist/social/richtext.js +100 -0
- package/dist/styles.d.ts +2 -0
- package/dist/styles.js +3 -0
- package/dist/theme.css +499 -0
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +23 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +6 -0
- package/package.json +108 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flo-bit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
7
|
+
the Software without restriction, including without limitation the rights to
|
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
9
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
10
|
+
so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Atmo Kit
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Svelte 5 and Tailwind CSS 4 components for Atmo apps.
|
|
4
|
+
|
|
5
|
+
- Install: `pnpm add @atmo-dev/kit`.
|
|
6
|
+
- Styles: import `@atmo-dev/kit/theme.css` after `tailwindcss`; wrap your app in `class="atmo zinc pink light"` (or `dark`).
|
|
7
|
+
- Develop: `pnpm install && pnpm dev`. Components at `/components`, app mockups at `/preview`.
|
|
8
|
+
- Validate: `pnpm lint && pnpm check && pnpm test:ci && pnpm build`.
|
|
9
|
+
- Releases: `pnpm changeset`, then merge the generated release PR. [Details](.changeset/README.md).
|
|
10
|
+
- License: [MIT](LICENSE).
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import { UserRound } from '@sketchyicons/svelte';
|
|
5
|
+
import Icon from './Icon.svelte';
|
|
6
|
+
let {
|
|
7
|
+
name,
|
|
8
|
+
src,
|
|
9
|
+
size = 36,
|
|
10
|
+
class: className,
|
|
11
|
+
...rest
|
|
12
|
+
}: Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
13
|
+
name: string;
|
|
14
|
+
src?: string;
|
|
15
|
+
size?: number;
|
|
16
|
+
} = $props();
|
|
17
|
+
let failedSrc = $state<string>();
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<span
|
|
21
|
+
{...rest}
|
|
22
|
+
class={cn(
|
|
23
|
+
'inline-grid size-(--ak-avatar-size) shrink-0 place-items-center overflow-hidden rounded-[45%_49%_44%_48%] border border-base-100 bg-accent-soft text-action dark:border-base-800',
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
style:--ak-avatar-size={`${size}px`}
|
|
27
|
+
role="img"
|
|
28
|
+
aria-label={name}
|
|
29
|
+
>
|
|
30
|
+
{#if src && src !== failedSrc}
|
|
31
|
+
<img {src} alt="" class="block size-full object-cover" onerror={() => (failedSrc = src)} />
|
|
32
|
+
{:else}
|
|
33
|
+
<Icon img={UserRound} class="size-3/5" />
|
|
34
|
+
{/if}
|
|
35
|
+
</span>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
|
|
3
|
+
name: string;
|
|
4
|
+
src?: string;
|
|
5
|
+
size?: number;
|
|
6
|
+
};
|
|
7
|
+
declare const Avatar: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
9
|
+
export default Avatar;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
let {
|
|
5
|
+
class: className,
|
|
6
|
+
variant = 'soft',
|
|
7
|
+
children,
|
|
8
|
+
...rest
|
|
9
|
+
}: HTMLAttributes<HTMLSpanElement> & { variant?: 'soft' | 'outline' } = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<span
|
|
13
|
+
{...rest}
|
|
14
|
+
class={cn(
|
|
15
|
+
'inline-flex w-fit items-center gap-1.5 rounded-md border px-2 py-0.75 text-[.6875rem] leading-normal font-medium',
|
|
16
|
+
variant === 'soft'
|
|
17
|
+
? 'border-action/20 bg-accent-soft text-action'
|
|
18
|
+
: 'border-control-line bg-transparent text-muted',
|
|
19
|
+
className
|
|
20
|
+
)}>{@render children?.()}</span
|
|
21
|
+
>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = HTMLAttributes<HTMLSpanElement> & {
|
|
3
|
+
variant?: 'soft' | 'outline';
|
|
4
|
+
};
|
|
5
|
+
declare const Badge: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
6
|
+
type Badge = ReturnType<typeof Badge>;
|
|
7
|
+
export default Badge;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
3
|
+
export type ButtonProps = Omit<HTMLButtonAttributes, 'onclick'> &
|
|
4
|
+
Omit<HTMLAnchorAttributes, 'type' | 'onclick'> & {
|
|
5
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'link';
|
|
6
|
+
size?: 'sm' | 'md' | 'lg' | 'icon';
|
|
7
|
+
loading?: boolean;
|
|
8
|
+
ref?: HTMLButtonElement | HTMLAnchorElement | null;
|
|
9
|
+
onclick?: (event: MouseEvent & { currentTarget: EventTarget & HTMLElement }) => void;
|
|
10
|
+
};
|
|
11
|
+
const variants = {
|
|
12
|
+
primary: 'bg-accent-400 text-base-950 hover:bg-accent-300 active:bg-accent-400',
|
|
13
|
+
secondary:
|
|
14
|
+
'border-line bg-surface text-ink hover:border-control-line hover:bg-recess active:bg-recess',
|
|
15
|
+
ghost: 'bg-transparent text-muted hover:bg-recess hover:text-ink active:bg-recess',
|
|
16
|
+
link: 'min-h-0 rounded px-0 py-1 text-action hover:underline underline-offset-4'
|
|
17
|
+
};
|
|
18
|
+
const sizes = {
|
|
19
|
+
sm: 'min-h-8.5 gap-1.5 px-3 py-1.5 text-xs',
|
|
20
|
+
md: 'min-h-10 px-3.5 py-2 text-[.8125rem]',
|
|
21
|
+
lg: 'min-h-11 px-5 py-2.5 text-sm',
|
|
22
|
+
icon: 'size-10 p-0'
|
|
23
|
+
};
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
import { cn } from '../utils.js';
|
|
28
|
+
import { focus } from '../styles.js';
|
|
29
|
+
import { LoaderCircle } from '@sketchyicons/svelte';
|
|
30
|
+
import Icon from './Icon.svelte';
|
|
31
|
+
let {
|
|
32
|
+
class: className,
|
|
33
|
+
variant = 'primary',
|
|
34
|
+
size = 'md',
|
|
35
|
+
loading = false,
|
|
36
|
+
disabled = false,
|
|
37
|
+
ref = $bindable(null),
|
|
38
|
+
href,
|
|
39
|
+
type = 'button',
|
|
40
|
+
onclick,
|
|
41
|
+
children,
|
|
42
|
+
...rest
|
|
43
|
+
}: ButtonProps = $props();
|
|
44
|
+
let classes = $derived(
|
|
45
|
+
cn(
|
|
46
|
+
'inline-flex shrink-0 cursor-pointer touch-manipulation items-center justify-center gap-2 rounded-(--atmo-radius-button) border border-transparent font-medium leading-snug no-underline transition-colors duration-(--atmo-duration) motion-reduce:transition-none disabled:pointer-events-none disabled:opacity-45 aria-disabled:pointer-events-none aria-disabled:opacity-45 aria-busy:opacity-100 [&_svg]:shrink-0',
|
|
47
|
+
focus,
|
|
48
|
+
sizes[size],
|
|
49
|
+
variants[variant],
|
|
50
|
+
className
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
{#snippet content()}
|
|
56
|
+
{#if loading}<Icon img={LoaderCircle} size={16} class="motion-safe:animate-spin" />{/if}
|
|
57
|
+
{@render children?.()}
|
|
58
|
+
{/snippet}
|
|
59
|
+
{#if href !== undefined}
|
|
60
|
+
<a
|
|
61
|
+
{...rest}
|
|
62
|
+
bind:this={ref}
|
|
63
|
+
href={disabled || loading ? undefined : href}
|
|
64
|
+
role={disabled || loading ? 'link' : undefined}
|
|
65
|
+
class={classes}
|
|
66
|
+
data-slot="button"
|
|
67
|
+
data-variant={variant}
|
|
68
|
+
data-size={size}
|
|
69
|
+
aria-disabled={disabled || loading ? true : rest['aria-disabled']}
|
|
70
|
+
aria-busy={loading || rest['aria-busy']}
|
|
71
|
+
tabindex={disabled || loading ? -1 : rest.tabindex}
|
|
72
|
+
onclick={(event) => {
|
|
73
|
+
if (disabled || loading) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopImmediatePropagation();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
onclick?.(event);
|
|
79
|
+
}}>{@render content()}</a
|
|
80
|
+
>
|
|
81
|
+
{:else}
|
|
82
|
+
<button
|
|
83
|
+
{...rest}
|
|
84
|
+
bind:this={ref}
|
|
85
|
+
{type}
|
|
86
|
+
disabled={disabled || loading}
|
|
87
|
+
class={classes}
|
|
88
|
+
data-slot="button"
|
|
89
|
+
data-variant={variant}
|
|
90
|
+
data-size={size}
|
|
91
|
+
aria-busy={loading || rest['aria-busy']}
|
|
92
|
+
{onclick}>{@render content()}</button
|
|
93
|
+
>
|
|
94
|
+
{/if}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
|
2
|
+
export type ButtonProps = Omit<HTMLButtonAttributes, 'onclick'> & Omit<HTMLAnchorAttributes, 'type' | 'onclick'> & {
|
|
3
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'link';
|
|
4
|
+
size?: 'sm' | 'md' | 'lg' | 'icon';
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
ref?: HTMLButtonElement | HTMLAnchorElement | null;
|
|
7
|
+
onclick?: (event: MouseEvent & {
|
|
8
|
+
currentTarget: EventTarget & HTMLElement;
|
|
9
|
+
}) => void;
|
|
10
|
+
};
|
|
11
|
+
declare const Button: import("svelte").Component<ButtonProps, {}, "ref">;
|
|
12
|
+
type Button = ReturnType<typeof Button>;
|
|
13
|
+
export default Button;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
import { focus } from '../styles.js';
|
|
5
|
+
let {
|
|
6
|
+
class: className,
|
|
7
|
+
checked = $bindable(false),
|
|
8
|
+
indeterminate = $bindable(false),
|
|
9
|
+
ref = $bindable(null),
|
|
10
|
+
...rest
|
|
11
|
+
}: Omit<HTMLInputAttributes, 'type'> & {
|
|
12
|
+
indeterminate?: boolean;
|
|
13
|
+
ref?: HTMLInputElement | null;
|
|
14
|
+
} = $props();
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<input
|
|
18
|
+
{...rest}
|
|
19
|
+
type="checkbox"
|
|
20
|
+
bind:checked
|
|
21
|
+
bind:indeterminate
|
|
22
|
+
bind:this={ref}
|
|
23
|
+
data-slot="checkbox"
|
|
24
|
+
class={cn(
|
|
25
|
+
'm-0 inline-block size-4.5 shrink-0 cursor-pointer appearance-auto align-middle accent-action focus:ring-0 focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-45',
|
|
26
|
+
focus,
|
|
27
|
+
className
|
|
28
|
+
)}
|
|
29
|
+
/>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = Omit<HTMLInputAttributes, 'type'> & {
|
|
3
|
+
indeterminate?: boolean;
|
|
4
|
+
ref?: HTMLInputElement | null;
|
|
5
|
+
};
|
|
6
|
+
declare const Checkbox: import("svelte").Component<$$ComponentProps, {}, "ref" | "checked" | "indeterminate">;
|
|
7
|
+
type Checkbox = ReturnType<typeof Checkbox>;
|
|
8
|
+
export default Checkbox;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
let {
|
|
5
|
+
class: className,
|
|
6
|
+
level = 1,
|
|
7
|
+
ref = $bindable(null),
|
|
8
|
+
children,
|
|
9
|
+
...rest
|
|
10
|
+
}: HTMLAttributes<HTMLHeadingElement> & {
|
|
11
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
12
|
+
ref?: HTMLHeadingElement | null;
|
|
13
|
+
} = $props();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<svelte:element
|
|
17
|
+
this={`h${level}`}
|
|
18
|
+
{...rest}
|
|
19
|
+
bind:this={ref}
|
|
20
|
+
class={cn(
|
|
21
|
+
'm-0 text-[clamp(2rem,4vw,2.75rem)] leading-[1.15] font-medium tracking-[-.035em] text-ink',
|
|
22
|
+
className
|
|
23
|
+
)}>{@render children?.()}</svelte:element
|
|
24
|
+
>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
type $$ComponentProps = HTMLAttributes<HTMLHeadingElement> & {
|
|
3
|
+
level?: 1 | 2 | 3 | 4 | 5 | 6;
|
|
4
|
+
ref?: HTMLHeadingElement | null;
|
|
5
|
+
};
|
|
6
|
+
declare const Heading: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
7
|
+
type Heading = ReturnType<typeof Heading>;
|
|
8
|
+
export default Heading;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SVGAttributes } from 'svelte/elements';
|
|
3
|
+
import { SketchyIcon, type SketchyIconProps } from '@sketchyicons/svelte';
|
|
4
|
+
let {
|
|
5
|
+
img,
|
|
6
|
+
size = 20,
|
|
7
|
+
strokeWidth = 1.75,
|
|
8
|
+
label,
|
|
9
|
+
'aria-label': ariaLabel,
|
|
10
|
+
...rest
|
|
11
|
+
}: SVGAttributes<SVGSVGElement> & SketchyIconProps & { label?: string } = $props();
|
|
12
|
+
let accessibleLabel = $derived(label ?? ariaLabel);
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<SketchyIcon
|
|
16
|
+
{img}
|
|
17
|
+
{size}
|
|
18
|
+
{strokeWidth}
|
|
19
|
+
{...rest}
|
|
20
|
+
role={accessibleLabel ? 'img' : rest.role}
|
|
21
|
+
aria-label={accessibleLabel}
|
|
22
|
+
aria-hidden={accessibleLabel ? undefined : (rest['aria-hidden'] ?? true)}
|
|
23
|
+
focusable="false"
|
|
24
|
+
/>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SVGAttributes } from 'svelte/elements';
|
|
2
|
+
import { type SketchyIconProps } from '@sketchyicons/svelte';
|
|
3
|
+
type $$ComponentProps = SVGAttributes<SVGSVGElement> & SketchyIconProps & {
|
|
4
|
+
label?: string;
|
|
5
|
+
};
|
|
6
|
+
declare const Icon: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
7
|
+
type Icon = ReturnType<typeof Icon>;
|
|
8
|
+
export default Icon;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button, { type ButtonProps } from './Button.svelte';
|
|
3
|
+
import { cn } from '../utils.js';
|
|
4
|
+
let {
|
|
5
|
+
label,
|
|
6
|
+
class: className,
|
|
7
|
+
variant = 'ghost',
|
|
8
|
+
size = 'md',
|
|
9
|
+
ref = $bindable(null),
|
|
10
|
+
children,
|
|
11
|
+
...rest
|
|
12
|
+
}: Omit<ButtonProps, 'size'> & { label: string; size?: 'sm' | 'md' | 'lg' } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<Button
|
|
16
|
+
{...rest}
|
|
17
|
+
bind:ref
|
|
18
|
+
{variant}
|
|
19
|
+
size="icon"
|
|
20
|
+
aria-label={label}
|
|
21
|
+
title={rest.title ?? label}
|
|
22
|
+
class={cn(size === 'sm' && 'size-8', size === 'lg' && 'size-12', className)}
|
|
23
|
+
>{@render children?.()}</Button
|
|
24
|
+
>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ButtonProps } from './Button.svelte';
|
|
2
|
+
type $$ComponentProps = Omit<ButtonProps, 'size'> & {
|
|
3
|
+
label: string;
|
|
4
|
+
size?: 'sm' | 'md' | 'lg';
|
|
5
|
+
};
|
|
6
|
+
declare const IconButton: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
7
|
+
type IconButton = ReturnType<typeof IconButton>;
|
|
8
|
+
export default IconButton;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { IconNode } from '@sketchyicons/svelte';
|
|
3
|
+
import type { ButtonProps } from './Button.svelte';
|
|
4
|
+
export type IconRailItem = Pick<ButtonProps, 'href' | 'onclick' | 'disabled'> & {
|
|
5
|
+
label: string;
|
|
6
|
+
icon: IconNode;
|
|
7
|
+
active?: boolean;
|
|
8
|
+
primary?: boolean;
|
|
9
|
+
badge?: boolean | number;
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { tick, type Snippet } from 'svelte';
|
|
15
|
+
import { MediaQuery } from 'svelte/reactivity';
|
|
16
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
17
|
+
import { Tooltip } from 'bits-ui';
|
|
18
|
+
import Button from './Button.svelte';
|
|
19
|
+
import Icon from './Icon.svelte';
|
|
20
|
+
import Modal from './Modal.svelte';
|
|
21
|
+
import { cn } from '../utils.js';
|
|
22
|
+
let {
|
|
23
|
+
label,
|
|
24
|
+
items,
|
|
25
|
+
backed = false,
|
|
26
|
+
mobile = 'bar',
|
|
27
|
+
open = $bindable(false),
|
|
28
|
+
footer,
|
|
29
|
+
class: className,
|
|
30
|
+
ref = $bindable(null),
|
|
31
|
+
...rest
|
|
32
|
+
}: Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
33
|
+
label: string;
|
|
34
|
+
items: readonly IconRailItem[];
|
|
35
|
+
backed?: boolean;
|
|
36
|
+
mobile?: 'bar' | 'drawer';
|
|
37
|
+
open?: boolean;
|
|
38
|
+
footer?: Snippet<[{ expanded: boolean; close: () => void }]>;
|
|
39
|
+
ref?: HTMLElement | null;
|
|
40
|
+
} = $props();
|
|
41
|
+
const small = new MediaQuery('(width < 40rem)', false);
|
|
42
|
+
const close = () => (open = false);
|
|
43
|
+
let drawerWasOpen = false;
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (mobile !== 'drawer') return;
|
|
46
|
+
const leavingDrawer = !small.current && drawerWasOpen;
|
|
47
|
+
drawerWasOpen = small.current && open;
|
|
48
|
+
if (!small.current) open = false;
|
|
49
|
+
if (leavingDrawer) {
|
|
50
|
+
void tick().then(() => {
|
|
51
|
+
if (!small.current)
|
|
52
|
+
ref?.querySelector<HTMLElement>('a[href], button:not(:disabled)')?.focus();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if mobile === 'drawer' && small.current}
|
|
59
|
+
<Modal
|
|
60
|
+
bind:open
|
|
61
|
+
title={label}
|
|
62
|
+
onCloseAutoFocus={(event) => {
|
|
63
|
+
if (!small.current) event.preventDefault();
|
|
64
|
+
}}
|
|
65
|
+
data-slot="icon-rail-drawer"
|
|
66
|
+
class="top-0 bottom-0 left-0 flex h-dvh max-h-none w-[min(19rem,calc(100%-2rem))] max-w-none translate-x-0 translate-y-0 flex-col rounded-none border-y-0 border-l-0 px-3 pt-[max(1rem,env(safe-area-inset-top))] pb-[max(1rem,env(safe-area-inset-bottom))] [&>header]:mb-3 [&>header]:px-2 [&>header_h2]:sr-only"
|
|
67
|
+
>
|
|
68
|
+
<nav
|
|
69
|
+
{...rest}
|
|
70
|
+
bind:this={ref}
|
|
71
|
+
aria-label={label}
|
|
72
|
+
data-slot="icon-rail"
|
|
73
|
+
class={cn('flex min-h-0 flex-1 flex-col overflow-y-auto overscroll-contain', className)}
|
|
74
|
+
>
|
|
75
|
+
<div class="grid gap-1">
|
|
76
|
+
{#each items as item (item.label)}
|
|
77
|
+
<Button
|
|
78
|
+
href={item.href}
|
|
79
|
+
disabled={item.disabled}
|
|
80
|
+
variant="ghost"
|
|
81
|
+
aria-current={item.active ? 'page' : undefined}
|
|
82
|
+
data-part="item"
|
|
83
|
+
onclick={(event) => {
|
|
84
|
+
close();
|
|
85
|
+
item.onclick?.(event);
|
|
86
|
+
}}
|
|
87
|
+
class={cn(
|
|
88
|
+
'min-h-11 w-full justify-start gap-3 px-3 py-3 text-sm aria-[current=page]:bg-recess aria-[current=page]:text-action',
|
|
89
|
+
item.primary && 'mt-3 text-action'
|
|
90
|
+
)}
|
|
91
|
+
>
|
|
92
|
+
<Icon img={item.icon} size={22} />
|
|
93
|
+
<span class="min-w-0 flex-1 text-left">{item.label}</span>
|
|
94
|
+
{#if item.badge}
|
|
95
|
+
<span class="text-xs text-action">
|
|
96
|
+
{#if typeof item.badge === 'number'}{item.badge}<span class="sr-only"> unread</span>
|
|
97
|
+
{:else}<span class="sr-only">Unread</span><span aria-hidden="true">•</span>{/if}
|
|
98
|
+
</span>
|
|
99
|
+
{/if}
|
|
100
|
+
</Button>
|
|
101
|
+
{/each}
|
|
102
|
+
</div>
|
|
103
|
+
{#if footer}<div class="mt-auto pt-6">{@render footer({ expanded: true, close })}</div>{/if}
|
|
104
|
+
</nav>
|
|
105
|
+
</Modal>
|
|
106
|
+
{:else}
|
|
107
|
+
<Tooltip.Provider delayDuration={350} ignoreNonKeyboardFocus>
|
|
108
|
+
<nav
|
|
109
|
+
{...rest}
|
|
110
|
+
bind:this={ref}
|
|
111
|
+
aria-label={label}
|
|
112
|
+
data-slot="icon-rail"
|
|
113
|
+
data-backed={backed}
|
|
114
|
+
class={cn(
|
|
115
|
+
'fixed top-[var(--atmo-nav-top,0px)] bottom-0 left-0 z-40 flex w-18 flex-col items-center bg-transparent pt-6 pb-5 max-sm:top-auto max-sm:right-0 max-sm:h-[calc(62px+env(safe-area-inset-bottom))] max-sm:w-full max-sm:flex-row max-sm:justify-around max-sm:bg-page max-sm:px-2 max-sm:pt-1.5 max-sm:pb-[calc(6px+env(safe-area-inset-bottom))]',
|
|
116
|
+
mobile === 'drawer' && 'max-sm:hidden',
|
|
117
|
+
className
|
|
118
|
+
)}
|
|
119
|
+
>
|
|
120
|
+
<div
|
|
121
|
+
class="flex flex-col items-center gap-3 max-sm:min-w-0 max-sm:flex-1 max-sm:flex-row max-sm:justify-around max-sm:gap-0"
|
|
122
|
+
>
|
|
123
|
+
{#each items as item (item.label)}
|
|
124
|
+
<Tooltip.Root disabled={item.disabled ?? false}
|
|
125
|
+
><Tooltip.Trigger onclick={item.onclick}
|
|
126
|
+
>{#snippet child({ props })}
|
|
127
|
+
<Button
|
|
128
|
+
{...props}
|
|
129
|
+
href={item.href}
|
|
130
|
+
disabled={item.disabled}
|
|
131
|
+
variant="ghost"
|
|
132
|
+
size="icon"
|
|
133
|
+
aria-label={`${item.label}${item.badge ? (typeof item.badge === 'number' ? `, ${item.badge} unread` : ', unread') : ''}`}
|
|
134
|
+
aria-current={item.active ? 'page' : undefined}
|
|
135
|
+
data-part="item"
|
|
136
|
+
class={cn(
|
|
137
|
+
'relative size-11 rounded-(--atmo-radius-button) text-muted hover:bg-recess/55 aria-[current=page]:text-action max-sm:size-10',
|
|
138
|
+
item.primary && 'mt-2 text-action hover:text-action max-sm:mt-0',
|
|
139
|
+
backed && 'bg-recess/45 hover:bg-recess/75'
|
|
140
|
+
)}
|
|
141
|
+
>
|
|
142
|
+
<Icon img={item.icon} size={22} />{#if item.badge}<span
|
|
143
|
+
class="absolute top-2 right-1.5 size-1.5 rounded-full bg-action"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
></span>{/if}
|
|
146
|
+
</Button>
|
|
147
|
+
{/snippet}</Tooltip.Trigger
|
|
148
|
+
><Tooltip.Content
|
|
149
|
+
role="tooltip"
|
|
150
|
+
side="right"
|
|
151
|
+
sideOffset={10}
|
|
152
|
+
collisionPadding={12}
|
|
153
|
+
class="z-60 max-w-[calc(100vw-2rem)] rounded-lg border border-line bg-field px-2.5 py-1.5 font-sans text-xs text-ink"
|
|
154
|
+
>{item.label}</Tooltip.Content
|
|
155
|
+
></Tooltip.Root
|
|
156
|
+
>
|
|
157
|
+
{/each}
|
|
158
|
+
</div>
|
|
159
|
+
{#if footer}<div class="mt-auto max-sm:mt-0 max-sm:ml-1">
|
|
160
|
+
{@render footer({ expanded: false, close })}
|
|
161
|
+
</div>{/if}
|
|
162
|
+
</nav>
|
|
163
|
+
</Tooltip.Provider>
|
|
164
|
+
{/if}
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
@keyframes drawer-enter {
|
|
168
|
+
from {
|
|
169
|
+
transform: translateX(-100%);
|
|
170
|
+
}
|
|
171
|
+
to {
|
|
172
|
+
transform: translateX(0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
:global([data-slot='icon-rail-drawer'][data-state='open']) {
|
|
176
|
+
animation: drawer-enter 130ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
177
|
+
}
|
|
178
|
+
@media (prefers-reduced-motion: reduce) {
|
|
179
|
+
:global([data-slot='icon-rail-drawer']) {
|
|
180
|
+
animation: none;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IconNode } from '@sketchyicons/svelte';
|
|
2
|
+
import type { ButtonProps } from './Button.svelte';
|
|
3
|
+
export type IconRailItem = Pick<ButtonProps, 'href' | 'onclick' | 'disabled'> & {
|
|
4
|
+
label: string;
|
|
5
|
+
icon: IconNode;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
primary?: boolean;
|
|
8
|
+
badge?: boolean | number;
|
|
9
|
+
};
|
|
10
|
+
import { type Snippet } from 'svelte';
|
|
11
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
12
|
+
type $$ComponentProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
13
|
+
label: string;
|
|
14
|
+
items: readonly IconRailItem[];
|
|
15
|
+
backed?: boolean;
|
|
16
|
+
mobile?: 'bar' | 'drawer';
|
|
17
|
+
open?: boolean;
|
|
18
|
+
footer?: Snippet<[{
|
|
19
|
+
expanded: boolean;
|
|
20
|
+
close: () => void;
|
|
21
|
+
}]>;
|
|
22
|
+
ref?: HTMLElement | null;
|
|
23
|
+
};
|
|
24
|
+
declare const IconRail: import("svelte").Component<$$ComponentProps, {}, "ref" | "open">;
|
|
25
|
+
type IconRail = ReturnType<typeof IconRail>;
|
|
26
|
+
export default IconRail;
|