@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,87 @@
|
|
|
1
|
+
import { cleanActors, isActorSummary } from './search.js';
|
|
2
|
+
export const RECENT_ACCOUNTS_STORAGE_KEY = 'atmo-kit:recent-accounts:v1';
|
|
3
|
+
function target(storage) {
|
|
4
|
+
try {
|
|
5
|
+
return storage === null
|
|
6
|
+
? undefined
|
|
7
|
+
: (storage ?? (typeof localStorage !== 'undefined' ? localStorage : undefined));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function recentAccountLimit(max = 4) {
|
|
14
|
+
return Number.isFinite(max) ? Math.max(0, Math.min(20, Math.trunc(max))) : 4;
|
|
15
|
+
}
|
|
16
|
+
export function loadRecentAccounts({ storage, key = RECENT_ACCOUNTS_STORAGE_KEY, max = 4 } = {}) {
|
|
17
|
+
try {
|
|
18
|
+
const data = JSON.parse(target(storage)?.getItem(key) ?? 'null');
|
|
19
|
+
if (!data || typeof data !== 'object')
|
|
20
|
+
return [];
|
|
21
|
+
// Accept Fox UI's origin-local recent-logins record when the caller selects that key.
|
|
22
|
+
const raw = 'version' in data && data.version === 1 && 'accounts' in data && Array.isArray(data.accounts)
|
|
23
|
+
? data.accounts
|
|
24
|
+
: Object.values(data);
|
|
25
|
+
const rows = raw.filter(isActorSummary).map((actor) => ({
|
|
26
|
+
...cleanActors([actor], 1)[0],
|
|
27
|
+
lastUsedAt: 'lastUsedAt' in actor &&
|
|
28
|
+
typeof actor.lastUsedAt === 'string' &&
|
|
29
|
+
Number.isFinite(Date.parse(actor.lastUsedAt))
|
|
30
|
+
? actor.lastUsedAt
|
|
31
|
+
: new Date(0).toISOString()
|
|
32
|
+
}));
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
return rows
|
|
35
|
+
.sort((a, b) => Date.parse(b.lastUsedAt) - Date.parse(a.lastUsedAt))
|
|
36
|
+
.filter((actor) => {
|
|
37
|
+
if (seen.has(actor.did))
|
|
38
|
+
return false;
|
|
39
|
+
seen.add(actor.did);
|
|
40
|
+
return true;
|
|
41
|
+
})
|
|
42
|
+
.slice(0, recentAccountLimit(max));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export function writeRecentAccounts(accounts, { storage, key = RECENT_ACCOUNTS_STORAGE_KEY, max = 4 } = {}) {
|
|
49
|
+
const seen = new Set();
|
|
50
|
+
const next = accounts
|
|
51
|
+
.filter(isActorSummary)
|
|
52
|
+
.filter((actor) => {
|
|
53
|
+
if (seen.has(actor.did))
|
|
54
|
+
return false;
|
|
55
|
+
seen.add(actor.did);
|
|
56
|
+
return true;
|
|
57
|
+
})
|
|
58
|
+
.slice(0, recentAccountLimit(max))
|
|
59
|
+
.map((actor) => ({
|
|
60
|
+
...cleanActors([actor], 1)[0],
|
|
61
|
+
lastUsedAt: 'lastUsedAt' in actor && typeof actor.lastUsedAt === 'string'
|
|
62
|
+
? actor.lastUsedAt
|
|
63
|
+
: new Date().toISOString()
|
|
64
|
+
}));
|
|
65
|
+
try {
|
|
66
|
+
const store = target(storage);
|
|
67
|
+
if (next.length)
|
|
68
|
+
store?.setItem(key, JSON.stringify({ version: 1, accounts: next }));
|
|
69
|
+
else
|
|
70
|
+
store?.removeItem(key);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* Blocked/full storage must not break login. */
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
export function saveRecentAccount(actor, options = {}) {
|
|
78
|
+
if (!isActorSummary(actor))
|
|
79
|
+
return loadRecentAccounts(options);
|
|
80
|
+
return writeRecentAccounts([
|
|
81
|
+
{ ...actor, lastUsedAt: new Date().toISOString() },
|
|
82
|
+
...loadRecentAccounts(options).filter((account) => account.did !== actor.did && account.handle !== actor.handle)
|
|
83
|
+
], options);
|
|
84
|
+
}
|
|
85
|
+
export function removeRecentAccount(did, options = {}) {
|
|
86
|
+
return writeRecentAccounts(loadRecentAccounts(options).filter((account) => account.did !== did), options);
|
|
87
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ActorSearch, ActorSummary } from './types.js';
|
|
2
|
+
export declare function isActorSummary(value: unknown): value is ActorSummary;
|
|
3
|
+
export declare function cleanActors(values: readonly unknown[], limit?: number): ActorSummary[];
|
|
4
|
+
/** Explicitly opt into public AppView search. The form never calls this by default. */
|
|
5
|
+
export declare function createActorSearch({ host, limit, fetch: fetcher }?: {
|
|
6
|
+
host?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
fetch?: typeof globalThis.fetch;
|
|
9
|
+
}): ActorSearch;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isValidHandle } from './handle.js';
|
|
2
|
+
export function isActorSummary(value) {
|
|
3
|
+
if (!value || typeof value !== 'object')
|
|
4
|
+
return false;
|
|
5
|
+
const actor = value;
|
|
6
|
+
return (typeof actor.did === 'string' &&
|
|
7
|
+
actor.did.startsWith('did:') &&
|
|
8
|
+
actor.did.length > 4 &&
|
|
9
|
+
typeof actor.handle === 'string' &&
|
|
10
|
+
actor.handle !== 'handle.invalid' &&
|
|
11
|
+
isValidHandle(actor.handle));
|
|
12
|
+
}
|
|
13
|
+
export function cleanActors(values, limit = 6) {
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
return values
|
|
16
|
+
.filter(isActorSummary)
|
|
17
|
+
.filter((actor) => {
|
|
18
|
+
if (seen.has(actor.did))
|
|
19
|
+
return false;
|
|
20
|
+
seen.add(actor.did);
|
|
21
|
+
return true;
|
|
22
|
+
})
|
|
23
|
+
.slice(0, limit)
|
|
24
|
+
.map((actor) => ({
|
|
25
|
+
did: actor.did,
|
|
26
|
+
handle: actor.handle,
|
|
27
|
+
displayName: typeof actor.displayName === 'string' ? actor.displayName : undefined,
|
|
28
|
+
avatar: typeof actor.avatar === 'string' && /^https?:\/\//i.test(actor.avatar)
|
|
29
|
+
? actor.avatar
|
|
30
|
+
: undefined
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
/** Explicitly opt into public AppView search. The form never calls this by default. */
|
|
34
|
+
export function createActorSearch({ host = 'https://public.api.bsky.app', limit = 6, fetch: fetcher = globalThis.fetch } = {}) {
|
|
35
|
+
const count = Number.isFinite(limit) ? Math.max(1, Math.min(25, Math.trunc(limit))) : 6;
|
|
36
|
+
return async (query, signal) => {
|
|
37
|
+
const url = new URL('/xrpc/app.bsky.actor.searchActorsTypeahead', host);
|
|
38
|
+
url.searchParams.set('q', query.trim());
|
|
39
|
+
url.searchParams.set('limit', String(count));
|
|
40
|
+
const response = await fetcher(url, { signal, headers: { accept: 'application/json' } });
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new Error('Account search is unavailable. You can still enter your full handle.');
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
const actors = data && typeof data === 'object' && 'actors' in data ? data.actors : undefined;
|
|
45
|
+
return Array.isArray(actors) ? cleanActors(actors, count) : [];
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IconNode } from '@sketchyicons/svelte';
|
|
2
|
+
/** Profile data for display, never evidence of an authenticated session. */
|
|
3
|
+
export type ActorSummary = {
|
|
4
|
+
did: string;
|
|
5
|
+
handle: string;
|
|
6
|
+
displayName?: string;
|
|
7
|
+
avatar?: string;
|
|
8
|
+
};
|
|
9
|
+
export type ActorSearch = (query: string, signal: AbortSignal) => Promise<readonly ActorSummary[]>;
|
|
10
|
+
export type LoginStep = 'login' | 'permissions' | 'signup';
|
|
11
|
+
export type LoginPermission = {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
description: string;
|
|
15
|
+
scopes: readonly string[];
|
|
16
|
+
icon?: IconNode;
|
|
17
|
+
required?: boolean;
|
|
18
|
+
defaultSelected?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Requested scopes, not proof of the permissions ultimately granted by OAuth. */
|
|
21
|
+
export type LoginOptions = {
|
|
22
|
+
permissionIds: string[];
|
|
23
|
+
scopes: string[];
|
|
24
|
+
};
|
|
25
|
+
export type LoginResult = boolean | void;
|
|
26
|
+
export type LoginCallback = (handle: string, actor?: ActorSummary, options?: LoginOptions) => LoginResult | Promise<LoginResult>;
|
|
27
|
+
export declare const actorName: (actor: ActorSummary) => string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const actorName = (actor) => actor.displayName?.trim() || `@${actor.handle}`;
|
package/dist/confetti.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Optional, explicit celebration. No global setup or effects on import. */
|
|
2
|
+
export async function launchConfetti() {
|
|
3
|
+
if (typeof window === 'undefined' ||
|
|
4
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
5
|
+
return;
|
|
6
|
+
const { default: confetti } = await import('canvas-confetti');
|
|
7
|
+
await confetti({
|
|
8
|
+
particleCount: 90,
|
|
9
|
+
spread: 65,
|
|
10
|
+
origin: { y: 0.7 },
|
|
11
|
+
disableForReducedMotion: true
|
|
12
|
+
});
|
|
13
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export { default as Button } from './components/Button.svelte';
|
|
2
|
+
export type { ButtonProps } from './components/Button.svelte';
|
|
3
|
+
export { default as IconButton } from './components/IconButton.svelte';
|
|
4
|
+
export { default as Input } from './components/Input.svelte';
|
|
5
|
+
export type { InputProps } from './components/Input.svelte';
|
|
6
|
+
export { default as Textarea } from './components/Textarea.svelte';
|
|
7
|
+
export { default as Select } from './components/Select.svelte';
|
|
8
|
+
export { default as Checkbox } from './components/Checkbox.svelte';
|
|
9
|
+
export { default as Label } from './components/Label.svelte';
|
|
10
|
+
export { default as PageTitle } from './components/PageTitle.svelte';
|
|
11
|
+
export { default as Heading } from './components/Heading.svelte';
|
|
12
|
+
export { default as Subheading } from './components/Subheading.svelte';
|
|
13
|
+
export { default as Surface } from './components/Surface.svelte';
|
|
14
|
+
export { default as Avatar } from './components/Avatar.svelte';
|
|
15
|
+
export { default as InsetImage } from './components/InsetImage.svelte';
|
|
16
|
+
export { default as Badge } from './components/Badge.svelte';
|
|
17
|
+
export { default as Modal } from './components/Modal.svelte';
|
|
18
|
+
export { default as Tabs } from './components/Tabs.svelte';
|
|
19
|
+
export type { TabItem } from './components/Tabs.svelte';
|
|
20
|
+
export { default as Icon } from './components/Icon.svelte';
|
|
21
|
+
export type { IconNode, SketchyIconProps } from '@sketchyicons/svelte';
|
|
22
|
+
export { default as IconRail } from './components/IconRail.svelte';
|
|
23
|
+
export type { IconRailItem } from './components/IconRail.svelte';
|
|
24
|
+
export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
|
|
25
|
+
export { default as LoginForm } from './components/login/LoginForm.svelte';
|
|
26
|
+
export type { LoginFormProps } from './components/login/LoginForm.svelte';
|
|
27
|
+
export { default as LoginDialog } from './components/login/LoginDialog.svelte';
|
|
28
|
+
export type { LoginDialogProps } from './components/login/LoginDialog.svelte';
|
|
29
|
+
export { default as ActorSearchField } from './components/login/ActorSearchField.svelte';
|
|
30
|
+
export { default as RecentAccounts } from './components/login/RecentAccounts.svelte';
|
|
31
|
+
export type { ActorSummary, ActorSearch, LoginCallback, LoginResult, LoginPermission, LoginOptions, LoginStep } from './components/login/types.js';
|
|
32
|
+
export { createActorSearch } from './components/login/search.js';
|
|
33
|
+
export { normalizeHandle, isValidHandle } from './components/login/handle.js';
|
|
34
|
+
export { resolveLoginOptions, isPermissionSelected } from './components/login/permissions.js';
|
|
35
|
+
export { loadRecentAccounts, writeRecentAccounts, saveRecentAccount, removeRecentAccount, RECENT_ACCOUNTS_STORAGE_KEY } from './components/login/recent-accounts.js';
|
|
36
|
+
export type { RecentAccount, RecentAccountStorage, RecentAccountStorageOptions } from './components/login/recent-accounts.js';
|
|
37
|
+
export { default as ToggleGroup } from './components/ToggleGroup.svelte';
|
|
38
|
+
export { default as ToggleGroupItem } from './components/ToggleGroupItem.svelte';
|
|
39
|
+
export { default as Popover } from './components/Popover.svelte';
|
|
40
|
+
export { default as LinkCard } from './components/LinkCard.svelte';
|
|
41
|
+
export { default as InlineEditor } from './components/InlineEditor.svelte';
|
|
42
|
+
export { default as PostComposer } from './components/PostComposer.svelte';
|
|
43
|
+
export { default as NestedComments } from './components/NestedComments.svelte';
|
|
44
|
+
export { default as PostContent } from './components/PostContent.svelte';
|
|
45
|
+
export { blueskyPostToPostData } from './social/posts.js';
|
|
46
|
+
export type { PostData, PostEmbed, BlueskyPostView } from './social/posts.js';
|
|
47
|
+
export { editorJsonToBlueskyPost, textToDoc, graphemeLength } from './social/richtext.js';
|
|
48
|
+
export type { Facet } from './social/richtext.js';
|
|
49
|
+
export type { Editor, JSONContent } from '@tiptap/core';
|
|
50
|
+
export { launchConfetti } from './confetti.js';
|
|
51
|
+
export { cn } from './utils.js';
|
|
52
|
+
export { baseColors, accentColors } from './palettes.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export { default as Button } from './components/Button.svelte';
|
|
2
|
+
export { default as IconButton } from './components/IconButton.svelte';
|
|
3
|
+
export { default as Input } from './components/Input.svelte';
|
|
4
|
+
export { default as Textarea } from './components/Textarea.svelte';
|
|
5
|
+
export { default as Select } from './components/Select.svelte';
|
|
6
|
+
export { default as Checkbox } from './components/Checkbox.svelte';
|
|
7
|
+
export { default as Label } from './components/Label.svelte';
|
|
8
|
+
export { default as PageTitle } from './components/PageTitle.svelte';
|
|
9
|
+
export { default as Heading } from './components/Heading.svelte';
|
|
10
|
+
export { default as Subheading } from './components/Subheading.svelte';
|
|
11
|
+
export { default as Surface } from './components/Surface.svelte';
|
|
12
|
+
export { default as Avatar } from './components/Avatar.svelte';
|
|
13
|
+
export { default as InsetImage } from './components/InsetImage.svelte';
|
|
14
|
+
export { default as Badge } from './components/Badge.svelte';
|
|
15
|
+
export { default as Modal } from './components/Modal.svelte';
|
|
16
|
+
export { default as Tabs } from './components/Tabs.svelte';
|
|
17
|
+
export { default as Icon } from './components/Icon.svelte';
|
|
18
|
+
export { default as IconRail } from './components/IconRail.svelte';
|
|
19
|
+
export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
|
|
20
|
+
export { default as LoginForm } from './components/login/LoginForm.svelte';
|
|
21
|
+
export { default as LoginDialog } from './components/login/LoginDialog.svelte';
|
|
22
|
+
export { default as ActorSearchField } from './components/login/ActorSearchField.svelte';
|
|
23
|
+
export { default as RecentAccounts } from './components/login/RecentAccounts.svelte';
|
|
24
|
+
export { createActorSearch } from './components/login/search.js';
|
|
25
|
+
export { normalizeHandle, isValidHandle } from './components/login/handle.js';
|
|
26
|
+
export { resolveLoginOptions, isPermissionSelected } from './components/login/permissions.js';
|
|
27
|
+
export { loadRecentAccounts, writeRecentAccounts, saveRecentAccount, removeRecentAccount, RECENT_ACCOUNTS_STORAGE_KEY } from './components/login/recent-accounts.js';
|
|
28
|
+
export { default as ToggleGroup } from './components/ToggleGroup.svelte';
|
|
29
|
+
export { default as ToggleGroupItem } from './components/ToggleGroupItem.svelte';
|
|
30
|
+
export { default as Popover } from './components/Popover.svelte';
|
|
31
|
+
export { default as LinkCard } from './components/LinkCard.svelte';
|
|
32
|
+
export { default as InlineEditor } from './components/InlineEditor.svelte';
|
|
33
|
+
export { default as PostComposer } from './components/PostComposer.svelte';
|
|
34
|
+
export { default as NestedComments } from './components/NestedComments.svelte';
|
|
35
|
+
export { default as PostContent } from './components/PostContent.svelte';
|
|
36
|
+
export { blueskyPostToPostData } from './social/posts.js';
|
|
37
|
+
export { editorJsonToBlueskyPost, textToDoc, graphemeLength } from './social/richtext.js';
|
|
38
|
+
export { launchConfetti } from './confetti.js';
|
|
39
|
+
export { cn } from './utils.js';
|
|
40
|
+
export { baseColors, accentColors } from './palettes.js';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const baseColors: readonly ["gray", "zinc", "neutral", "stone", "slate", "mist", "mauve", "olive", "taupe"];
|
|
2
|
+
export declare const accentColors: readonly ["red", "orange", "amber", "yellow", "lime", "green", "emerald", "teal", "cyan", "sky", "blue", "indigo", "violet", "purple", "fuchsia", "pink", "rose"];
|
package/dist/palettes.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const baseColors = [
|
|
2
|
+
'gray',
|
|
3
|
+
'zinc',
|
|
4
|
+
'neutral',
|
|
5
|
+
'stone',
|
|
6
|
+
'slate',
|
|
7
|
+
'mist',
|
|
8
|
+
'mauve',
|
|
9
|
+
'olive',
|
|
10
|
+
'taupe'
|
|
11
|
+
];
|
|
12
|
+
export const accentColors = [
|
|
13
|
+
'red',
|
|
14
|
+
'orange',
|
|
15
|
+
'amber',
|
|
16
|
+
'yellow',
|
|
17
|
+
'lime',
|
|
18
|
+
'green',
|
|
19
|
+
'emerald',
|
|
20
|
+
'teal',
|
|
21
|
+
'cyan',
|
|
22
|
+
'sky',
|
|
23
|
+
'blue',
|
|
24
|
+
'indigo',
|
|
25
|
+
'violet',
|
|
26
|
+
'purple',
|
|
27
|
+
'fuchsia',
|
|
28
|
+
'pink',
|
|
29
|
+
'rose'
|
|
30
|
+
];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/core';
|
|
2
|
+
import type { ActorSearch, ActorSummary } from '../components/login/types.js';
|
|
3
|
+
/** DOM is constructed with textContent: profile data never becomes HTML. */
|
|
4
|
+
export declare function mentionSuggestions(search: ActorSearch, host: () => HTMLElement): {
|
|
5
|
+
char: string;
|
|
6
|
+
debounce: number;
|
|
7
|
+
minQueryLength: number;
|
|
8
|
+
items: ({ query }: {
|
|
9
|
+
query: string;
|
|
10
|
+
}) => Promise<ActorSummary[]>;
|
|
11
|
+
render: () => {
|
|
12
|
+
onStart: (props: {
|
|
13
|
+
items: ActorSummary[];
|
|
14
|
+
command: (actor: {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
}) => void;
|
|
18
|
+
clientRect?: (() => DOMRect | null) | null;
|
|
19
|
+
editor: Editor;
|
|
20
|
+
}) => void;
|
|
21
|
+
onUpdate: (props: {
|
|
22
|
+
items: ActorSummary[];
|
|
23
|
+
command: (actor: {
|
|
24
|
+
id: string;
|
|
25
|
+
label: string;
|
|
26
|
+
}) => void;
|
|
27
|
+
clientRect?: (() => DOMRect | null) | null;
|
|
28
|
+
editor: Editor;
|
|
29
|
+
}) => void;
|
|
30
|
+
onKeyDown: ({ event }: {
|
|
31
|
+
event: KeyboardEvent;
|
|
32
|
+
}) => boolean;
|
|
33
|
+
onExit: () => void;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** DOM is constructed with textContent: profile data never becomes HTML. */
|
|
2
|
+
export function mentionSuggestions(search, host) {
|
|
3
|
+
let controller;
|
|
4
|
+
let menu;
|
|
5
|
+
let selected = 0;
|
|
6
|
+
let current;
|
|
7
|
+
const close = () => {
|
|
8
|
+
menu?.remove();
|
|
9
|
+
menu = undefined;
|
|
10
|
+
current?.editor.view.dom.removeAttribute('aria-controls');
|
|
11
|
+
current?.editor.view.dom.removeAttribute('aria-activedescendant');
|
|
12
|
+
current?.editor.view.dom.setAttribute('aria-expanded', 'false');
|
|
13
|
+
};
|
|
14
|
+
const choose = (index) => {
|
|
15
|
+
const actor = current.items[index];
|
|
16
|
+
if (actor)
|
|
17
|
+
current.command({ id: actor.did, label: actor.handle });
|
|
18
|
+
};
|
|
19
|
+
function draw() {
|
|
20
|
+
if (!menu)
|
|
21
|
+
return;
|
|
22
|
+
menu.replaceChildren();
|
|
23
|
+
const rect = current.clientRect?.();
|
|
24
|
+
const bounds = host().getBoundingClientRect();
|
|
25
|
+
if (rect) {
|
|
26
|
+
menu.style.top = `${rect.bottom - bounds.top + host().scrollTop + 6}px`;
|
|
27
|
+
menu.style.left = `${Math.max(0, Math.min(rect.left - bounds.left, bounds.width - 280))}px`;
|
|
28
|
+
}
|
|
29
|
+
if (!current.items.length) {
|
|
30
|
+
const empty = document.createElement('p');
|
|
31
|
+
empty.role = 'option';
|
|
32
|
+
empty.setAttribute('aria-disabled', 'true');
|
|
33
|
+
empty.setAttribute('aria-selected', 'false');
|
|
34
|
+
empty.className = 'p-3 text-xs text-muted';
|
|
35
|
+
empty.textContent = 'No accounts found';
|
|
36
|
+
menu.append(empty);
|
|
37
|
+
}
|
|
38
|
+
current.items.forEach((actor, index) => {
|
|
39
|
+
const option = document.createElement('button');
|
|
40
|
+
option.type = 'button';
|
|
41
|
+
option.role = 'option';
|
|
42
|
+
option.id = `${menu.id}-${index}`;
|
|
43
|
+
option.setAttribute('aria-selected', String(index === selected));
|
|
44
|
+
option.className =
|
|
45
|
+
'block w-full rounded-ui px-3 py-2 text-left text-xs text-ink hover:bg-recess aria-selected:bg-recess';
|
|
46
|
+
const name = document.createElement('strong');
|
|
47
|
+
name.className = 'block truncate font-medium';
|
|
48
|
+
name.textContent = actor.displayName || actor.handle;
|
|
49
|
+
const handle = document.createElement('span');
|
|
50
|
+
handle.className = 'block truncate text-muted';
|
|
51
|
+
handle.textContent = `@${actor.handle}`;
|
|
52
|
+
option.append(name, handle);
|
|
53
|
+
option.onmousedown = (event) => event.preventDefault();
|
|
54
|
+
option.onclick = () => choose(index);
|
|
55
|
+
menu.append(option);
|
|
56
|
+
});
|
|
57
|
+
current.editor.view.dom.setAttribute('aria-controls', menu.id);
|
|
58
|
+
current.editor.view.dom.setAttribute('aria-expanded', 'true');
|
|
59
|
+
if (current.items.length)
|
|
60
|
+
current.editor.view.dom.setAttribute('aria-activedescendant', `${menu.id}-${selected}`);
|
|
61
|
+
else
|
|
62
|
+
current.editor.view.dom.removeAttribute('aria-activedescendant');
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
char: '@',
|
|
66
|
+
debounce: 200,
|
|
67
|
+
minQueryLength: 1,
|
|
68
|
+
items: async ({ query }) => {
|
|
69
|
+
controller?.abort();
|
|
70
|
+
controller = new AbortController();
|
|
71
|
+
try {
|
|
72
|
+
return [...(await search(query, controller.signal))].slice(0, 6);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
render: () => ({
|
|
79
|
+
onStart: (props) => {
|
|
80
|
+
current = props;
|
|
81
|
+
selected = 0;
|
|
82
|
+
menu = document.createElement('div');
|
|
83
|
+
menu.id = `${props.editor.view.dom.id}-mentions`;
|
|
84
|
+
menu.role = 'listbox';
|
|
85
|
+
menu.setAttribute('aria-label', 'Mention suggestions');
|
|
86
|
+
menu.className =
|
|
87
|
+
'absolute z-60 w-70 max-w-full overflow-hidden rounded-ui border border-line bg-page p-1 font-sans text-ink';
|
|
88
|
+
host().append(menu);
|
|
89
|
+
draw();
|
|
90
|
+
},
|
|
91
|
+
onUpdate: (props) => {
|
|
92
|
+
current = props;
|
|
93
|
+
selected = 0;
|
|
94
|
+
draw();
|
|
95
|
+
},
|
|
96
|
+
onKeyDown: ({ event }) => {
|
|
97
|
+
if (event.key === 'Escape') {
|
|
98
|
+
close();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (!current.items.length)
|
|
102
|
+
return false;
|
|
103
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
104
|
+
selected =
|
|
105
|
+
(selected + (event.key === 'ArrowDown' ? 1 : -1) + current.items.length) %
|
|
106
|
+
current.items.length;
|
|
107
|
+
draw();
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (event.key === 'Enter' || event.key === 'Tab') {
|
|
111
|
+
choose(selected);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
},
|
|
116
|
+
onExit: () => {
|
|
117
|
+
controller?.abort();
|
|
118
|
+
close();
|
|
119
|
+
}
|
|
120
|
+
})
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { Facet } from './richtext.js';
|
|
2
|
+
export type PostEmbed = {
|
|
3
|
+
type: 'images';
|
|
4
|
+
images: {
|
|
5
|
+
src: string;
|
|
6
|
+
fullsize?: string;
|
|
7
|
+
alt: string;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
}[];
|
|
11
|
+
} | {
|
|
12
|
+
type: 'external';
|
|
13
|
+
uri: string;
|
|
14
|
+
title: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
image?: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'video';
|
|
19
|
+
playlist: string;
|
|
20
|
+
poster?: string;
|
|
21
|
+
alt?: string;
|
|
22
|
+
} | {
|
|
23
|
+
type: 'quote';
|
|
24
|
+
post?: PostData;
|
|
25
|
+
unavailable?: boolean;
|
|
26
|
+
};
|
|
27
|
+
export type PostData = {
|
|
28
|
+
uri: string;
|
|
29
|
+
href: string;
|
|
30
|
+
author: {
|
|
31
|
+
did: string;
|
|
32
|
+
handle: string;
|
|
33
|
+
name: string;
|
|
34
|
+
avatar?: string;
|
|
35
|
+
};
|
|
36
|
+
text: string;
|
|
37
|
+
facets?: Facet[];
|
|
38
|
+
createdAt?: string;
|
|
39
|
+
replyCount?: number;
|
|
40
|
+
likeCount?: number;
|
|
41
|
+
embeds?: PostEmbed[];
|
|
42
|
+
replies?: PostData[];
|
|
43
|
+
};
|
|
44
|
+
/** The public AppView view shape; no session or network dependency. */
|
|
45
|
+
export type BlueskyPostView = {
|
|
46
|
+
uri: string;
|
|
47
|
+
author: {
|
|
48
|
+
did: string;
|
|
49
|
+
handle: string;
|
|
50
|
+
displayName?: string;
|
|
51
|
+
avatar?: string;
|
|
52
|
+
};
|
|
53
|
+
record?: {
|
|
54
|
+
text?: string;
|
|
55
|
+
facets?: Facet[];
|
|
56
|
+
createdAt?: string;
|
|
57
|
+
};
|
|
58
|
+
value?: {
|
|
59
|
+
text?: string;
|
|
60
|
+
facets?: Facet[];
|
|
61
|
+
createdAt?: string;
|
|
62
|
+
};
|
|
63
|
+
embed?: BlueskyEmbedView;
|
|
64
|
+
embeds?: BlueskyEmbedView[];
|
|
65
|
+
replyCount?: number;
|
|
66
|
+
likeCount?: number;
|
|
67
|
+
};
|
|
68
|
+
type BlueskyEmbedView = {
|
|
69
|
+
$type?: string;
|
|
70
|
+
images?: {
|
|
71
|
+
thumb: string;
|
|
72
|
+
fullsize?: string;
|
|
73
|
+
alt: string;
|
|
74
|
+
aspectRatio?: {
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
};
|
|
78
|
+
}[];
|
|
79
|
+
external?: {
|
|
80
|
+
uri: string;
|
|
81
|
+
title: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
thumb?: string;
|
|
84
|
+
};
|
|
85
|
+
playlist?: string;
|
|
86
|
+
thumbnail?: string;
|
|
87
|
+
alt?: string;
|
|
88
|
+
record?: BlueskyPostView & {
|
|
89
|
+
$type?: string;
|
|
90
|
+
record?: BlueskyPostView;
|
|
91
|
+
};
|
|
92
|
+
media?: BlueskyEmbedView;
|
|
93
|
+
};
|
|
94
|
+
export declare function blueskyPostToPostData(post: BlueskyPostView, depth?: number): PostData;
|
|
95
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function blueskyPostToPostData(post, depth = 0) {
|
|
2
|
+
const record = post.record ?? post.value;
|
|
3
|
+
const result = {
|
|
4
|
+
uri: post.uri,
|
|
5
|
+
href: `https://bsky.app/profile/${encodeURIComponent(post.author.did)}/post/${encodeURIComponent(post.uri.split('/').at(-1) ?? '')}`,
|
|
6
|
+
author: {
|
|
7
|
+
did: post.author.did,
|
|
8
|
+
handle: post.author.handle,
|
|
9
|
+
name: post.author.displayName || post.author.handle,
|
|
10
|
+
avatar: post.author.avatar
|
|
11
|
+
},
|
|
12
|
+
text: record?.text ?? '',
|
|
13
|
+
facets: record?.facets,
|
|
14
|
+
createdAt: record?.createdAt,
|
|
15
|
+
replyCount: post.replyCount,
|
|
16
|
+
likeCount: post.likeCount,
|
|
17
|
+
embeds: []
|
|
18
|
+
};
|
|
19
|
+
function add(embed) {
|
|
20
|
+
if (embed.images)
|
|
21
|
+
result.embeds.push({
|
|
22
|
+
type: 'images',
|
|
23
|
+
images: embed.images.map((image) => ({
|
|
24
|
+
src: image.thumb,
|
|
25
|
+
fullsize: image.fullsize,
|
|
26
|
+
alt: image.alt,
|
|
27
|
+
...image.aspectRatio
|
|
28
|
+
}))
|
|
29
|
+
});
|
|
30
|
+
if (embed.external)
|
|
31
|
+
result.embeds.push({
|
|
32
|
+
type: 'external',
|
|
33
|
+
uri: embed.external.uri,
|
|
34
|
+
title: embed.external.title,
|
|
35
|
+
description: embed.external.description,
|
|
36
|
+
image: embed.external.thumb
|
|
37
|
+
});
|
|
38
|
+
if (embed.playlist)
|
|
39
|
+
result.embeds.push({
|
|
40
|
+
type: 'video',
|
|
41
|
+
playlist: embed.playlist,
|
|
42
|
+
poster: embed.thumbnail,
|
|
43
|
+
alt: embed.alt
|
|
44
|
+
});
|
|
45
|
+
if (embed.media)
|
|
46
|
+
add(embed.media);
|
|
47
|
+
if (embed.record && depth < 3) {
|
|
48
|
+
const quote = embed.record.record ?? embed.record;
|
|
49
|
+
result.embeds.push({
|
|
50
|
+
type: 'quote',
|
|
51
|
+
post: quote.author && quote.uri ? blueskyPostToPostData(quote, depth + 1) : undefined,
|
|
52
|
+
unavailable: !quote.author
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const embed of post.embeds ?? (post.embed ? [post.embed] : []))
|
|
57
|
+
add(embed);
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { JSONContent } from '@tiptap/core';
|
|
2
|
+
export type Facet = {
|
|
3
|
+
index: {
|
|
4
|
+
byteStart: number;
|
|
5
|
+
byteEnd: number;
|
|
6
|
+
};
|
|
7
|
+
features: {
|
|
8
|
+
$type: string;
|
|
9
|
+
did?: string;
|
|
10
|
+
uri?: string;
|
|
11
|
+
tag?: string;
|
|
12
|
+
}[];
|
|
13
|
+
};
|
|
14
|
+
export declare function safeWebUrl(url: string | undefined): string | undefined;
|
|
15
|
+
export declare function textToDoc(text: string): JSONContent;
|
|
16
|
+
/** Serialize mention nodes first, then detect links/tags using UTF-8 byte offsets. */
|
|
17
|
+
export declare function editorJsonToBlueskyPost(doc: JSONContent): {
|
|
18
|
+
text: string;
|
|
19
|
+
facets: Facet[];
|
|
20
|
+
};
|
|
21
|
+
export declare function richTextSegments(text: string, facets?: Facet[]): {
|
|
22
|
+
text: string;
|
|
23
|
+
href?: string;
|
|
24
|
+
}[];
|
|
25
|
+
export declare function graphemeLength(text: string): number;
|