@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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +9 -2
  3. package/dist/components/Avatar.svelte +35 -0
  4. package/dist/components/Avatar.svelte.d.ts +9 -0
  5. package/dist/components/Badge.svelte +21 -0
  6. package/dist/components/Badge.svelte.d.ts +7 -0
  7. package/dist/components/Button.svelte +94 -0
  8. package/dist/components/Button.svelte.d.ts +13 -0
  9. package/dist/components/Checkbox.svelte +29 -0
  10. package/dist/components/Checkbox.svelte.d.ts +8 -0
  11. package/dist/components/Heading.svelte +24 -0
  12. package/dist/components/Heading.svelte.d.ts +8 -0
  13. package/dist/components/Icon.svelte +24 -0
  14. package/dist/components/Icon.svelte.d.ts +8 -0
  15. package/dist/components/IconButton.svelte +24 -0
  16. package/dist/components/IconButton.svelte.d.ts +8 -0
  17. package/dist/components/IconRail.svelte +183 -0
  18. package/dist/components/IconRail.svelte.d.ts +26 -0
  19. package/dist/components/InlineEditor.svelte +120 -0
  20. package/dist/components/InlineEditor.svelte.d.ts +19 -0
  21. package/dist/components/Input.svelte +32 -0
  22. package/dist/components/Input.svelte.d.ts +9 -0
  23. package/dist/components/InsetImage.svelte +29 -0
  24. package/dist/components/InsetImage.svelte.d.ts +9 -0
  25. package/dist/components/Label.svelte +19 -0
  26. package/dist/components/Label.svelte.d.ts +7 -0
  27. package/dist/components/LinkCard.svelte +47 -0
  28. package/dist/components/LinkCard.svelte.d.ts +12 -0
  29. package/dist/components/Modal.svelte +93 -0
  30. package/dist/components/Modal.svelte.d.ts +12 -0
  31. package/dist/components/NestedComments.svelte +61 -0
  32. package/dist/components/NestedComments.svelte.d.ts +8 -0
  33. package/dist/components/PageTitle.svelte +22 -0
  34. package/dist/components/PageTitle.svelte.d.ts +8 -0
  35. package/dist/components/Popover.svelte +49 -0
  36. package/dist/components/Popover.svelte.d.ts +12 -0
  37. package/dist/components/PostComposer.svelte +41 -0
  38. package/dist/components/PostComposer.svelte.d.ts +16 -0
  39. package/dist/components/PostContent.svelte +62 -0
  40. package/dist/components/PostContent.svelte.d.ts +8 -0
  41. package/dist/components/PostVideo.svelte +74 -0
  42. package/dist/components/PostVideo.svelte.d.ts +9 -0
  43. package/dist/components/Select.svelte +34 -0
  44. package/dist/components/Select.svelte.d.ts +9 -0
  45. package/dist/components/Subheading.svelte +22 -0
  46. package/dist/components/Subheading.svelte.d.ts +8 -0
  47. package/dist/components/Surface.svelte +40 -0
  48. package/dist/components/Surface.svelte.d.ts +11 -0
  49. package/dist/components/Tabs.svelte +47 -0
  50. package/dist/components/Tabs.svelte.d.ts +16 -0
  51. package/dist/components/Textarea.svelte +20 -0
  52. package/dist/components/Textarea.svelte.d.ts +7 -0
  53. package/dist/components/ToggleGroup.svelte +32 -0
  54. package/dist/components/ToggleGroup.svelte.d.ts +12 -0
  55. package/dist/components/ToggleGroupItem.svelte +26 -0
  56. package/dist/components/ToggleGroupItem.svelte.d.ts +8 -0
  57. package/dist/components/WorkspaceSidebar.svelte +82 -0
  58. package/dist/components/WorkspaceSidebar.svelte.d.ts +10 -0
  59. package/dist/components/login/ActorSearchField.svelte +238 -0
  60. package/dist/components/login/ActorSearchField.svelte.d.ts +19 -0
  61. package/dist/components/login/AtmosphereApps.svelte +19 -0
  62. package/dist/components/login/AtmosphereApps.svelte.d.ts +18 -0
  63. package/dist/components/login/LoginDialog.svelte +78 -0
  64. package/dist/components/login/LoginDialog.svelte.d.ts +9 -0
  65. package/dist/components/login/LoginForm.svelte +402 -0
  66. package/dist/components/login/LoginForm.svelte.d.ts +28 -0
  67. package/dist/components/login/RecentAccounts.svelte +57 -0
  68. package/dist/components/login/RecentAccounts.svelte.d.ts +10 -0
  69. package/dist/components/login/brands/README.md +7 -0
  70. package/dist/components/login/brands/blacksky.svg +3 -0
  71. package/dist/components/login/brands/bluesky.svg +3 -0
  72. package/dist/components/login/brands/bookhive.svg +3 -0
  73. package/dist/components/login/brands/graze.svg +3 -0
  74. package/dist/components/login/brands/leaflet.svg +3 -0
  75. package/dist/components/login/brands/pckt.svg +3 -0
  76. package/dist/components/login/brands/surf.svg +3 -0
  77. package/dist/components/login/brands/tangled.svg +3 -0
  78. package/dist/components/login/handle.d.ts +3 -0
  79. package/dist/components/login/handle.js +11 -0
  80. package/dist/components/login/permissions.d.ts +3 -0
  81. package/dist/components/login/permissions.js +13 -0
  82. package/dist/components/login/recent-accounts.d.ts +16 -0
  83. package/dist/components/login/recent-accounts.js +87 -0
  84. package/dist/components/login/search.d.ts +9 -0
  85. package/dist/components/login/search.js +47 -0
  86. package/dist/components/login/types.d.ts +27 -0
  87. package/dist/components/login/types.js +1 -0
  88. package/dist/confetti.d.ts +2 -0
  89. package/dist/confetti.js +13 -0
  90. package/dist/index.d.ts +52 -0
  91. package/dist/index.js +40 -0
  92. package/dist/palettes.d.ts +2 -0
  93. package/dist/palettes.js +30 -0
  94. package/dist/social/mention-suggestions.d.ts +35 -0
  95. package/dist/social/mention-suggestions.js +122 -0
  96. package/dist/social/posts.d.ts +95 -0
  97. package/dist/social/posts.js +59 -0
  98. package/dist/social/richtext.d.ts +25 -0
  99. package/dist/social/richtext.js +100 -0
  100. package/dist/styles.d.ts +2 -0
  101. package/dist/styles.js +3 -0
  102. package/dist/theme.css +499 -0
  103. package/dist/theme.d.ts +2 -0
  104. package/dist/theme.js +23 -0
  105. package/dist/utils.d.ts +2 -0
  106. package/dist/utils.js +6 -0
  107. 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
- # Temporary Holding Version
1
+ # Atmo Kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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;