@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
|
@@ -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,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;
|