@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
@@ -0,0 +1,120 @@
1
+ <script lang="ts">
2
+ import { onMount, untrack } from 'svelte';
3
+ import { Editor, type JSONContent } from '@tiptap/core';
4
+ import Document from '@tiptap/extension-document';
5
+ import Paragraph from '@tiptap/extension-paragraph';
6
+ import Text from '@tiptap/extension-text';
7
+ import HardBreak from '@tiptap/extension-hard-break';
8
+ import History from '@tiptap/extension-history';
9
+ import Placeholder from '@tiptap/extension-placeholder';
10
+ import Mention from '@tiptap/extension-mention';
11
+ import type { ActorSearch } from './login/types.js';
12
+ import { mentionSuggestions } from '../social/mention-suggestions.js';
13
+ import { editorJsonToBlueskyPost, textToDoc } from '../social/richtext.js';
14
+ import { cn } from '../utils.js';
15
+ let {
16
+ value = $bindable(''),
17
+ editor = $bindable<Editor | null>(null),
18
+ label,
19
+ placeholder = '',
20
+ singleLine = false,
21
+ disabled = false,
22
+ searchActors,
23
+ onupdate,
24
+ class: className
25
+ }: {
26
+ value?: string;
27
+ editor?: Editor | null;
28
+ label: string;
29
+ placeholder?: string;
30
+ singleLine?: boolean;
31
+ disabled?: boolean;
32
+ searchActors?: ActorSearch;
33
+ onupdate?: (content: { text: string; json: JSONContent }) => void;
34
+ class?: string;
35
+ } = $props();
36
+ const id = $props.id();
37
+ let host: HTMLDivElement;
38
+ let element: HTMLDivElement;
39
+ onMount(() => {
40
+ const instance = new Editor({
41
+ element,
42
+ extensions: [
43
+ Document,
44
+ Paragraph,
45
+ Text,
46
+ HardBreak,
47
+ History,
48
+ Placeholder.configure({ placeholder }),
49
+ ...(searchActors
50
+ ? [
51
+ Mention.configure({
52
+ HTMLAttributes: { class: 'text-action' },
53
+ suggestion: mentionSuggestions(searchActors, () => host)
54
+ })
55
+ ]
56
+ : [])
57
+ ],
58
+ content: textToDoc(untrack(() => value)),
59
+ editable: !disabled,
60
+ editorProps: {
61
+ attributes: {
62
+ id,
63
+ role: searchActors ? 'combobox' : 'textbox',
64
+ 'aria-label': label,
65
+ ...(searchActors
66
+ ? { 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'aria-haspopup': 'listbox' }
67
+ : { 'aria-multiline': String(!singleLine) }),
68
+ class:
69
+ 'min-h-[1.5em] outline-none whitespace-pre-wrap break-words [&_p.is-editor-empty:first-child]:before:content-[attr(data-placeholder)] [&_p.is-editor-empty:first-child]:before:text-muted [&_p.is-editor-empty:first-child]:before:float-left [&_p.is-editor-empty:first-child]:before:pointer-events-none [&_p.is-editor-empty:first-child]:before:h-0'
70
+ },
71
+ handleKeyDown: (view, event) =>
72
+ singleLine &&
73
+ event.key === 'Enter' &&
74
+ !event.isComposing &&
75
+ !view.dom.getAttribute('aria-activedescendant'),
76
+ transformPastedText: (text) => (singleLine ? text.replace(/[\r\n]+/g, ' ') : text),
77
+ // Accept plain text only, preserving the editor's deliberately small schema.
78
+ handlePaste: (_view, event) => {
79
+ const text = event.clipboardData?.getData('text/plain');
80
+ if (text === undefined) return false;
81
+ event.preventDefault();
82
+ if (singleLine) {
83
+ const cleaned = text.replace(/[\r\n]+/g, ' ');
84
+ if (cleaned) instance.commands.insertContent({ type: 'text', text: cleaned });
85
+ } else instance.commands.insertContent(textToDoc(text).content ?? []);
86
+ return true;
87
+ }
88
+ },
89
+ onUpdate: ({ editor: current }) => {
90
+ const json = current.getJSON();
91
+ value = editorJsonToBlueskyPost(json).text;
92
+ onupdate?.({ text: value, json });
93
+ }
94
+ });
95
+ editor = instance;
96
+ return () => {
97
+ instance.destroy();
98
+ editor = null;
99
+ };
100
+ });
101
+ $effect(() => {
102
+ if (editor && editorJsonToBlueskyPost(editor.getJSON()).text !== value)
103
+ editor.commands.setContent(textToDoc(value), { emitUpdate: false });
104
+ });
105
+ $effect(() => {
106
+ editor?.setEditable(!disabled, false);
107
+ editor?.view.dom.setAttribute('aria-disabled', String(disabled));
108
+ });
109
+ </script>
110
+
111
+ <div
112
+ bind:this={host}
113
+ class={cn(
114
+ 'relative min-w-0 rounded-sm focus-within:outline-2 focus-within:outline-offset-4 focus-within:outline-focus',
115
+ className
116
+ )}
117
+ data-slot="inline-editor"
118
+ >
119
+ <div bind:this={element}></div>
120
+ </div>
@@ -0,0 +1,19 @@
1
+ import { Editor, type JSONContent } from '@tiptap/core';
2
+ import type { ActorSearch } from './login/types.js';
3
+ type $$ComponentProps = {
4
+ value?: string;
5
+ editor?: Editor | null;
6
+ label: string;
7
+ placeholder?: string;
8
+ singleLine?: boolean;
9
+ disabled?: boolean;
10
+ searchActors?: ActorSearch;
11
+ onupdate?: (content: {
12
+ text: string;
13
+ json: JSONContent;
14
+ }) => void;
15
+ class?: string;
16
+ };
17
+ declare const InlineEditor: import("svelte").Component<$$ComponentProps, {}, "value" | "editor">;
18
+ type InlineEditor = ReturnType<typeof InlineEditor>;
19
+ export default InlineEditor;
@@ -0,0 +1,32 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
3
+ export type InputProps = Omit<HTMLInputAttributes, 'type' | 'value'> & {
4
+ type?: Exclude<
5
+ HTMLInputTypeAttribute,
6
+ 'checkbox' | 'radio' | 'file' | 'button' | 'submit' | 'reset' | 'image' | 'range'
7
+ >;
8
+ value?: string | number;
9
+ ref?: HTMLInputElement | null;
10
+ };
11
+ </script>
12
+
13
+ <script lang="ts">
14
+ import { cn } from '../utils.js';
15
+ import { control } from '../styles.js';
16
+ let {
17
+ class: className,
18
+ value = $bindable<string | number | undefined>(''),
19
+ type = 'text',
20
+ ref = $bindable(null),
21
+ ...rest
22
+ }: InputProps = $props();
23
+ </script>
24
+
25
+ <input
26
+ {...rest}
27
+ {type}
28
+ bind:value
29
+ bind:this={ref}
30
+ data-slot="input"
31
+ class={cn(control, className)}
32
+ />
@@ -0,0 +1,9 @@
1
+ import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
2
+ export type InputProps = Omit<HTMLInputAttributes, 'type' | 'value'> & {
3
+ type?: Exclude<HTMLInputTypeAttribute, 'checkbox' | 'radio' | 'file' | 'button' | 'submit' | 'reset' | 'image' | 'range'>;
4
+ value?: string | number;
5
+ ref?: HTMLInputElement | null;
6
+ };
7
+ declare const Input: import("svelte").Component<InputProps, {}, "value" | "ref">;
8
+ type Input = ReturnType<typeof Input>;
9
+ export default Input;
@@ -0,0 +1,29 @@
1
+ <script lang="ts">
2
+ import type { HTMLImgAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils.js';
4
+ let {
5
+ src,
6
+ alt,
7
+ ratio = 1,
8
+ class: className,
9
+ style,
10
+ loading = 'lazy',
11
+ ...rest
12
+ }: Omit<HTMLImgAttributes, 'src' | 'alt'> & {
13
+ src: string;
14
+ alt: string;
15
+ ratio?: number | string;
16
+ } = $props();
17
+ </script>
18
+
19
+ <figure
20
+ class={cn(
21
+ 'relative m-0 aspect-(--ak-image-ratio) overflow-hidden rounded-ui border border-line after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:shadow-[inset_0_2px_7px_#0005]',
22
+ className
23
+ )}
24
+ {style}
25
+ style:--ak-image-ratio={ratio}
26
+ data-slot="inset-image"
27
+ >
28
+ <img {...rest} {src} {alt} {loading} class="block size-full object-cover" />
29
+ </figure>
@@ -0,0 +1,9 @@
1
+ import type { HTMLImgAttributes } from 'svelte/elements';
2
+ type $$ComponentProps = Omit<HTMLImgAttributes, 'src' | 'alt'> & {
3
+ src: string;
4
+ alt: string;
5
+ ratio?: number | string;
6
+ };
7
+ declare const InsetImage: import("svelte").Component<$$ComponentProps, {}, "">;
8
+ type InsetImage = ReturnType<typeof InsetImage>;
9
+ export default InsetImage;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import type { HTMLLabelAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils.js';
4
+ let {
5
+ class: className,
6
+ ref = $bindable(null),
7
+ children,
8
+ ...rest
9
+ }: HTMLLabelAttributes & { ref?: HTMLLabelElement | null } = $props();
10
+ </script>
11
+
12
+ <label
13
+ {...rest}
14
+ bind:this={ref}
15
+ class={cn(
16
+ 'inline-flex items-center gap-2.5 text-[.8125rem] leading-normal font-medium text-ink',
17
+ className
18
+ )}>{@render children?.()}</label
19
+ >
@@ -0,0 +1,7 @@
1
+ import type { HTMLLabelAttributes } from 'svelte/elements';
2
+ type $$ComponentProps = HTMLLabelAttributes & {
3
+ ref?: HTMLLabelElement | null;
4
+ };
5
+ declare const Label: import("svelte").Component<$$ComponentProps, {}, "ref">;
6
+ type Label = ReturnType<typeof Label>;
7
+ export default Label;
@@ -0,0 +1,47 @@
1
+ <script lang="ts">
2
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
3
+ import { cn } from '../utils.js';
4
+ import { focus } from '../styles.js';
5
+ import { safeWebUrl } from '../social/richtext.js';
6
+ let {
7
+ href,
8
+ meta,
9
+ class: className,
10
+ ...rest
11
+ }: Omit<HTMLAnchorAttributes, 'href' | 'children'> & {
12
+ href: string;
13
+ meta: { title?: string; description?: string; image?: string };
14
+ } = $props();
15
+ </script>
16
+
17
+ <a
18
+ {...rest}
19
+ href={safeWebUrl(href)}
20
+ target="_blank"
21
+ rel="noopener noreferrer"
22
+ class={cn(
23
+ 'block overflow-hidden rounded-ui border border-line bg-surface text-ink no-underline hover:border-control-line',
24
+ focus,
25
+ className
26
+ )}
27
+ >
28
+ {#if meta.image && safeWebUrl(meta.image)}<div class="relative">
29
+ <img
30
+ src={meta.image}
31
+ alt=""
32
+ class="aspect-[1.91/1] w-full object-cover"
33
+ loading="lazy"
34
+ /><span
35
+ class="pointer-events-none absolute inset-0 [box-shadow:inset_0_0_0_1px_var(--ui-shadow),inset_0_2px_5px_var(--ui-shadow)]"
36
+ ></span>
37
+ </div>{/if}
38
+ <div class="p-3">
39
+ <strong class="block text-sm font-medium">{meta.title || href}</strong>
40
+ {#if meta.description}<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted">
41
+ {meta.description}
42
+ </p>{/if}
43
+ <span class="mt-1 block truncate text-[.6875rem] text-muted"
44
+ >{href.replace(/^https?:\/\//, '')}</span
45
+ >
46
+ </div>
47
+ </a>
@@ -0,0 +1,12 @@
1
+ import type { HTMLAnchorAttributes } from 'svelte/elements';
2
+ type $$ComponentProps = Omit<HTMLAnchorAttributes, 'href' | 'children'> & {
3
+ href: string;
4
+ meta: {
5
+ title?: string;
6
+ description?: string;
7
+ image?: string;
8
+ };
9
+ };
10
+ declare const LinkCard: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type LinkCard = ReturnType<typeof LinkCard>;
12
+ export default LinkCard;
@@ -0,0 +1,93 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { Dialog as Primitive } from 'bits-ui';
4
+ import Icon from './Icon.svelte';
5
+ import { X } from '@sketchyicons/svelte';
6
+ import { cn } from '../utils.js';
7
+ import { focus } from '../styles.js';
8
+ import { readTheme, observeTheme } from '../theme.js';
9
+ let {
10
+ open = $bindable(false),
11
+ title,
12
+ description,
13
+ dismissible = true,
14
+ class: className,
15
+ style,
16
+ children,
17
+ onOpenAutoFocus,
18
+ ...rest
19
+ }: Omit<Primitive.ContentProps, 'children' | 'child' | 'title'> & {
20
+ open?: boolean;
21
+ title: string;
22
+ description?: string;
23
+ dismissible?: boolean;
24
+ children?: Snippet;
25
+ } = $props();
26
+ let origin = $state<HTMLSpanElement>();
27
+ let content = $state<HTMLElement | null>(null);
28
+ let inherited = $state('');
29
+ $effect(() => {
30
+ if (!open || !origin) {
31
+ inherited = '';
32
+ return;
33
+ }
34
+ const element = origin;
35
+ const sync = () => (inherited = readTheme(element));
36
+ sync();
37
+ return observeTheme(element, sync);
38
+ });
39
+ </script>
40
+
41
+ <span hidden bind:this={origin} class={cn('atmo-theme', className)} {style}></span>
42
+ <Primitive.Root bind:open>
43
+ {#if inherited}<Primitive.Portal>
44
+ <Primitive.Overlay class="fixed inset-0 z-50 bg-black/55" />
45
+ <Primitive.Content
46
+ {...rest}
47
+ bind:ref={content}
48
+ interactOutsideBehavior={dismissible ? 'close' : 'ignore'}
49
+ escapeKeydownBehavior={dismissible ? 'close' : 'ignore'}
50
+ style={[inherited, style].filter(Boolean).join(';')}
51
+ class={cn(
52
+ 'fixed top-1/2 left-1/2 z-50 max-h-[calc(100dvh-3rem)] w-[calc(100%-2rem)] max-w-lg -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-panel border border-line bg-page p-6 font-sans text-ink outline-none sm:p-7',
53
+ className
54
+ )}
55
+ onOpenAutoFocus={(event) => {
56
+ onOpenAutoFocus?.(event);
57
+ if (!event.defaultPrevented) {
58
+ const target = content?.querySelector<HTMLElement>('[data-autofocus]');
59
+ if (target) {
60
+ event.preventDefault();
61
+ target.focus();
62
+ }
63
+ }
64
+ }}
65
+ >
66
+ <header class="mb-6 flex items-start justify-between gap-4">
67
+ <div class="min-w-0 flex-1">
68
+ <Primitive.Title>
69
+ {#snippet child({ props })}<h2
70
+ {...props}
71
+ tabindex="-1"
72
+ class="text-[1.375rem] leading-snug font-medium tracking-tight break-words outline-none"
73
+ >
74
+ {title}
75
+ </h2>{/snippet}
76
+ </Primitive.Title>{#if description}<Primitive.Description
77
+ class="mt-2.5 text-[.8125rem] leading-relaxed text-muted"
78
+ >{description}</Primitive.Description
79
+ >{/if}
80
+ </div>
81
+ <Primitive.Close
82
+ disabled={!dismissible}
83
+ aria-label="Close dialog"
84
+ class={cn(
85
+ '-mt-1 -mr-2 inline-grid size-9 shrink-0 cursor-pointer place-items-center rounded-ui text-muted hover:bg-recess hover:text-ink disabled:pointer-events-none disabled:opacity-40',
86
+ focus
87
+ )}><Icon img={X} size={19} /></Primitive.Close
88
+ >
89
+ </header>
90
+ {@render children?.()}
91
+ </Primitive.Content>
92
+ </Primitive.Portal>{/if}
93
+ </Primitive.Root>
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { Dialog as Primitive } from 'bits-ui';
3
+ type $$ComponentProps = Omit<Primitive.ContentProps, 'children' | 'child' | 'title'> & {
4
+ open?: boolean;
5
+ title: string;
6
+ description?: string;
7
+ dismissible?: boolean;
8
+ children?: Snippet;
9
+ };
10
+ declare const Modal: import("svelte").Component<$$ComponentProps, {}, "open">;
11
+ type Modal = ReturnType<typeof Modal>;
12
+ export default Modal;
@@ -0,0 +1,61 @@
1
+ <script lang="ts">
2
+ import type { PostData } from '../social/posts.js';
3
+ import { safeWebUrl } from '../social/richtext.js';
4
+ import Avatar from './Avatar.svelte';
5
+ import PostContent from './PostContent.svelte';
6
+ import NestedComments from './NestedComments.svelte';
7
+ let { comments }: { comments: PostData[] } = $props();
8
+ </script>
9
+
10
+ <div class="space-y-6" data-slot="comments">
11
+ {#each comments as comment (comment.uri)}
12
+ <article class="min-w-0">
13
+ <div class="flex items-start gap-3">
14
+ <a
15
+ href={`https://bsky.app/profile/${encodeURIComponent(comment.author.did)}`}
16
+ target="_blank"
17
+ rel="noopener noreferrer"
18
+ class="shrink-0"
19
+ ><Avatar name={comment.author.name} src={comment.author.avatar} size={32} /></a
20
+ >
21
+ <div class="min-w-0 flex-1">
22
+ <div class="mb-1 flex flex-wrap items-baseline gap-x-2 text-xs">
23
+ <a
24
+ href={safeWebUrl(comment.href)}
25
+ target="_blank"
26
+ rel="noopener noreferrer"
27
+ class="font-medium text-ink hover:underline">{comment.author.name}</a
28
+ ><span class="truncate text-muted">@{comment.author.handle}</span
29
+ >{#if comment.createdAt && !Number.isNaN(Date.parse(comment.createdAt))}<time
30
+ datetime={comment.createdAt}
31
+ class="text-[.6875rem] text-muted"
32
+ >{new Date(comment.createdAt).toLocaleDateString(undefined, {
33
+ month: 'short',
34
+ day: 'numeric'
35
+ })}</time
36
+ >{/if}
37
+ </div>
38
+ <PostContent post={comment} />
39
+ <div class="mt-2 flex gap-5 text-[.6875rem] text-muted">
40
+ <a
41
+ href={safeWebUrl(comment.href)}
42
+ target="_blank"
43
+ rel="noopener noreferrer"
44
+ class="hover:text-action"
45
+ >Reply{#if comment.replyCount}
46
+ · {comment.replyCount}{/if}</a
47
+ ><a
48
+ href={safeWebUrl(comment.href)}
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ class="hover:text-action">{comment.likeCount ?? 0} likes</a
52
+ >
53
+ </div>
54
+ </div>
55
+ </div>
56
+ {#if comment.replies?.length}<div class="mt-4 border-l border-line pl-3 sm:ml-4 sm:pl-6">
57
+ <NestedComments comments={comment.replies} />
58
+ </div>{/if}
59
+ </article>
60
+ {/each}
61
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { PostData } from '../social/posts.js';
2
+ import NestedComments from './NestedComments.svelte';
3
+ type $$ComponentProps = {
4
+ comments: PostData[];
5
+ };
6
+ declare const NestedComments: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type NestedComments = ReturnType<typeof NestedComments>;
8
+ export default NestedComments;
@@ -0,0 +1,22 @@
1
+ <script lang="ts">
2
+ let {
3
+ title,
4
+ siteName,
5
+ separator = ' — '
6
+ }: {
7
+ title?: string | null;
8
+ siteName: string;
9
+ separator?: string;
10
+ } = $props();
11
+
12
+ let normalizedTitle = $derived(title?.trim());
13
+ let pageTitle = $derived(
14
+ !normalizedTitle || normalizedTitle === siteName
15
+ ? siteName
16
+ : `${normalizedTitle}${separator}${siteName}`
17
+ );
18
+ </script>
19
+
20
+ <svelte:head>
21
+ <title>{pageTitle}</title>
22
+ </svelte:head>
@@ -0,0 +1,8 @@
1
+ type $$ComponentProps = {
2
+ title?: string | null;
3
+ siteName: string;
4
+ separator?: string;
5
+ };
6
+ declare const PageTitle: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type PageTitle = ReturnType<typeof PageTitle>;
8
+ export default PageTitle;
@@ -0,0 +1,49 @@
1
+ <script lang="ts">
2
+ import { Popover as Primitive } from 'bits-ui';
3
+ import type { Snippet } from 'svelte';
4
+ import { cn } from '../utils.js';
5
+ import { readTheme, observeTheme } from '../theme.js';
6
+ let {
7
+ open = $bindable(false),
8
+ trigger,
9
+ children,
10
+ class: className,
11
+ style,
12
+ role = 'dialog',
13
+ sideOffset = 8,
14
+ ...rest
15
+ }: Omit<Primitive.ContentProps, 'children' | 'child'> & {
16
+ open?: boolean;
17
+ trigger: Snippet<[{ props: Record<string, unknown> }]>;
18
+ children?: Snippet;
19
+ } = $props();
20
+ let origin = $state<HTMLSpanElement>();
21
+ let inherited = $state('');
22
+ $effect(() => {
23
+ if (!open || !origin) return;
24
+ const sync = () => (inherited = readTheme(origin!));
25
+ sync();
26
+ return observeTheme(origin, sync);
27
+ });
28
+ </script>
29
+
30
+ <span hidden bind:this={origin} class={cn('atmo-theme', className)} {style}></span>
31
+ <Primitive.Root bind:open>
32
+ <Primitive.Trigger
33
+ >{#snippet child({ props })}{@render trigger({ props })}{/snippet}</Primitive.Trigger
34
+ >
35
+ <Primitive.Portal>
36
+ <Primitive.Content
37
+ {...rest}
38
+ {role}
39
+ {sideOffset}
40
+ style={[inherited, style].filter(Boolean).join(';')}
41
+ class={cn(
42
+ 'z-50 max-w-[calc(100vw-2rem)] rounded-panel border border-line bg-page p-4 font-sans text-ink outline-none',
43
+ className
44
+ )}
45
+ >
46
+ {@render children?.()}
47
+ </Primitive.Content>
48
+ </Primitive.Portal>
49
+ </Primitive.Root>
@@ -0,0 +1,12 @@
1
+ import { Popover as Primitive } from 'bits-ui';
2
+ import type { Snippet } from 'svelte';
3
+ type $$ComponentProps = Omit<Primitive.ContentProps, 'children' | 'child'> & {
4
+ open?: boolean;
5
+ trigger: Snippet<[{
6
+ props: Record<string, unknown>;
7
+ }]>;
8
+ children?: Snippet;
9
+ };
10
+ declare const Popover: import("svelte").Component<$$ComponentProps, {}, "open">;
11
+ type Popover = ReturnType<typeof Popover>;
12
+ export default Popover;
@@ -0,0 +1,41 @@
1
+ <script lang="ts">
2
+ import type { Editor, JSONContent } from '@tiptap/core';
3
+ import InlineEditor from './InlineEditor.svelte';
4
+ import type { ActorSearch } from './login/types.js';
5
+ import { graphemeLength } from '../social/richtext.js';
6
+ let {
7
+ value = $bindable(''),
8
+ editor = $bindable<Editor | null>(null),
9
+ searchActors,
10
+ maxLength = 300,
11
+ disabled = false,
12
+ onupdate
13
+ }: {
14
+ value?: string;
15
+ editor?: Editor | null;
16
+ searchActors?: ActorSearch;
17
+ maxLength?: number;
18
+ disabled?: boolean;
19
+ onupdate?: (content: { text: string; json: JSONContent }) => void;
20
+ } = $props();
21
+ let length = $derived(graphemeLength(value));
22
+ </script>
23
+
24
+ <InlineEditor
25
+ bind:value
26
+ bind:editor
27
+ label="Post text"
28
+ placeholder="What’s happening?"
29
+ {searchActors}
30
+ {disabled}
31
+ {onupdate}
32
+ class="min-h-24 text-sm leading-relaxed"
33
+ />
34
+ <p
35
+ class={length > maxLength
36
+ ? 'red mt-2 text-right text-xs text-action'
37
+ : 'mt-2 text-right text-xs text-muted'}
38
+ aria-live="polite"
39
+ >
40
+ {length} / {maxLength}
41
+ </p>
@@ -0,0 +1,16 @@
1
+ import type { Editor, JSONContent } from '@tiptap/core';
2
+ import type { ActorSearch } from './login/types.js';
3
+ type $$ComponentProps = {
4
+ value?: string;
5
+ editor?: Editor | null;
6
+ searchActors?: ActorSearch;
7
+ maxLength?: number;
8
+ disabled?: boolean;
9
+ onupdate?: (content: {
10
+ text: string;
11
+ json: JSONContent;
12
+ }) => void;
13
+ };
14
+ declare const PostComposer: import("svelte").Component<$$ComponentProps, {}, "value" | "editor">;
15
+ type PostComposer = ReturnType<typeof PostComposer>;
16
+ export default PostComposer;