@atmo-dev/kit 0.0.2 → 0.2.0
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/README.md +99 -0
- package/dist/components/legal/LegalDocument.svelte +58 -0
- package/dist/components/legal/LegalDocument.svelte.d.ts +8 -0
- package/dist/components/legal/LegalFooter.svelte +34 -0
- package/dist/components/legal/LegalFooter.svelte.d.ts +11 -0
- package/dist/components/legal/drafts.d.ts +10 -0
- package/dist/components/legal/drafts.js +134 -0
- package/dist/components/legal/links.d.ts +5 -0
- package/dist/components/legal/links.js +39 -0
- package/dist/components/legal/types.d.ts +39 -0
- package/dist/components/legal/types.js +1 -0
- package/dist/components/login/ActorSearchField.svelte +5 -4
- package/dist/components/login/AtmosphereApps.svelte +3 -2
- package/dist/components/login/LoginDialog.svelte +3 -3
- package/dist/components/login/LoginForm.svelte +25 -48
- package/dist/components/login/LoginForm.svelte.d.ts +2 -1
- package/dist/components/login/PermissionFields.svelte +56 -0
- package/dist/components/login/PermissionFields.svelte.d.ts +9 -0
- package/dist/components/login/SignupPermissions.svelte +51 -0
- package/dist/components/login/SignupPermissions.svelte.d.ts +10 -0
- package/dist/components/login/brands/README.md +3 -1
- package/dist/components/login/brands/mu-social.svg +12 -0
- package/dist/components/login/handle.d.ts +5 -0
- package/dist/components/login/handle.js +19 -1
- package/dist/components/login/types.d.ts +2 -1
- package/dist/components/report/ReportDialog.svelte +140 -0
- package/dist/components/report/ReportDialog.svelte.d.ts +15 -0
- package/dist/components/report/email.d.ts +3 -0
- package/dist/components/report/email.js +6 -0
- package/dist/components/report/types.d.ts +11 -0
- package/dist/components/report/types.js +1 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +6 -1
- package/dist/legal.d.ts +3 -0
- package/dist/legal.js +2 -0
- package/dist/theme.css +1 -0
- package/dist/theme.js +8 -1
- package/package.json +5 -1
- package/dist/components/login/brands/bookhive.svg +0 -3
package/README.md
CHANGED
|
@@ -8,3 +8,102 @@ Svelte 5 and Tailwind CSS 4 components for Atmo apps.
|
|
|
8
8
|
- Validate: `pnpm lint && pnpm check && pnpm test:ci && pnpm build`.
|
|
9
9
|
- Releases: `pnpm changeset`, then merge the generated release PR. [Details](.changeset/README.md).
|
|
10
10
|
- License: [MIT](LICENSE).
|
|
11
|
+
|
|
12
|
+
## Login identifiers
|
|
13
|
+
|
|
14
|
+
`LoginForm` and `LoginDialog` accept handles and DIDs by default. Handles are trimmed, stripped of leading `@` characters, and lowercased. DIDs are trimmed only: method-specific case and percent encoding are preserved. `initialHandle` also accepts a DID; its existing name is retained for compatibility.
|
|
15
|
+
|
|
16
|
+
The `login(identifier, actor?, options?)` and `onSuccess(identifier, actor?)` callbacks receive that identifier unchanged after normalization. A manually entered DID does not fabricate profile data or a recent-account record. Optional permission review works the same for either identifier.
|
|
17
|
+
|
|
18
|
+
Validation is syntactic, not proof that an account exists. Apps own DID resolution, supported DID methods, OAuth, and sessions. DID inputs bypass optional handle-search providers and cancel pending suggestions.
|
|
19
|
+
|
|
20
|
+
For other login entry points, import `normalizeIdentifier` and `isValidIdentifier`. Existing `normalizeHandle` and `isValidHandle` remain handle-only helpers; do not use them on DIDs.
|
|
21
|
+
|
|
22
|
+
## Legal pages and footer
|
|
23
|
+
|
|
24
|
+
`LegalDocument` renders app-owned document data with readable typography and an optional app-reviewed revision date. Publication status is metadata, not a visible page label. `LegalFooter` renders only configured links. Preview the actual pages at `/legal`; authoring configuration and diagnostics have their own view.
|
|
25
|
+
|
|
26
|
+
```svelte
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { LegalDocument, LegalFooter } from '@atmo-dev/kit';
|
|
29
|
+
import { privacy } from './reviewed-policies';
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<LegalDocument document={privacy} />
|
|
33
|
+
<LegalFooter
|
|
34
|
+
links={{
|
|
35
|
+
terms: '/terms',
|
|
36
|
+
privacy: '/privacy',
|
|
37
|
+
imprint: '/imprint',
|
|
38
|
+
moderation: false,
|
|
39
|
+
report: '/report',
|
|
40
|
+
source: { label: 'Source code', href: 'https://github.com/example/project' }
|
|
41
|
+
}}
|
|
42
|
+
/>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Every destination is optional: omit it or set it to `false` to hide it. A string uses the default label; `{ label, href }` customizes it. Plain `mailto:` destinations are accepted for reporting. Unsafe URLs are not made clickable. No links/actions means no empty footer. The footer forwards native attributes and `class`.
|
|
46
|
+
|
|
47
|
+
An app-owned `reportAction` snippet replaces `links.report` with a contextual button/dialog trigger. Setting `links.report: false` disables that action too. The footer owns no session, reporting subject, dialog, or network operation.
|
|
48
|
+
|
|
49
|
+
### Author and review documents
|
|
50
|
+
|
|
51
|
+
Import framework-free helpers from `@atmo-dev/kit/legal` in a script:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { createLegalDrafts, getLegalDraftIssues } from '@atmo-dev/kit/legal';
|
|
55
|
+
|
|
56
|
+
const config = {
|
|
57
|
+
app: { name: 'Example', url: 'https://example.com', description: 'an event app' },
|
|
58
|
+
operator: { name: 'Operator', email: 'hello@example.com', address: [] },
|
|
59
|
+
privacy: [
|
|
60
|
+
{
|
|
61
|
+
id: 'storage',
|
|
62
|
+
title: 'Data storage',
|
|
63
|
+
paragraphs: [
|
|
64
|
+
'Describe actual data, purposes, recipients, retention, legal bases, and safeguards.'
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
moderation: [
|
|
69
|
+
{
|
|
70
|
+
id: 'reports',
|
|
71
|
+
title: 'Reporting',
|
|
72
|
+
paragraphs: ['Describe the working report channel and review procedure.']
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
};
|
|
76
|
+
console.error(getLegalDraftIssues(config));
|
|
77
|
+
console.log(JSON.stringify(createLegalDrafts(config), null, 2));
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Save generated documents in the consuming app, review/edit their wording, and render that snapshot—not a runtime call to `createLegalDrafts`. **A Kit upgrade must not silently rewrite a published policy.** The templates are drafts, not legal advice or a guarantee of compliance. They infer no jurisdiction, age threshold, legal basis, retention period, processor agreement, acceptance flow, or notice commitment. Operator facts can be reused across apps; each app still needs its actual processing and terms reviewed.
|
|
81
|
+
|
|
82
|
+
`LegalDocumentData` contains a `title`, `status: 'draft' | 'published'`, optional app-owned `updated` date, and `sections`. Each section has a unique `id`, `title`, and optional `paragraphs`, `items`, `address`, and `links`. Text is escaped, not rendered as HTML. Templates provide terms, privacy, imprint, and moderation drafts, but apps can supply their own text/documents and external policy links instead.
|
|
83
|
+
|
|
84
|
+
Set `status: 'published'` and an ISO `updated` date only after review. `getLegalDocumentIssues` checks publication fields and section identifiers; `getLegalDraftIssues` checks authoring configuration. Neither determines legal compliance. Routes, page titles, indexing of draft pages, notice/acceptance mechanisms, and enforcement remain app-owned.
|
|
85
|
+
|
|
86
|
+
## Reporting
|
|
87
|
+
|
|
88
|
+
`ReportDialog` owns the reason/details form, pending and success states, and the signed-out email fallback. Apps own the trigger, sessions, subject references, canonical links, mailbox, and server submission.
|
|
89
|
+
|
|
90
|
+
```svelte
|
|
91
|
+
<ReportDialog
|
|
92
|
+
bind:open
|
|
93
|
+
isLoggedIn={viewer.isLoggedIn}
|
|
94
|
+
email="reports@example.com"
|
|
95
|
+
subjectUrl={canonicalUrl}
|
|
96
|
+
onsignin={showLogin}
|
|
97
|
+
submit={async (input) => {
|
|
98
|
+
try {
|
|
99
|
+
await sendReport({ subject, ...input });
|
|
100
|
+
} catch {
|
|
101
|
+
return { message: 'Reporting is unavailable. Please report by email.' };
|
|
102
|
+
}
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`submit` receives `{ reasonType, reason? }`, using the standard ATProto spam, violation, and other reason identifiers. Blank details are omitted; entered details are trimmed and limited to 2,000 characters. Resolve with no value after a successful save, or return `{ message, needsSignIn? }` for a safe, user-facing failure. `needsSignIn` offers a sign-in-again action. Unexpected rejections show a generic failure without exposing exception details.
|
|
108
|
+
|
|
109
|
+
The email fallback prefills the subject link and any entered details. Opening a new dialog or changing `subjectUrl` resets its state; stale async results are ignored. The dialog does not import an app's auth singleton, contact a moderation service, or decide who can report a subject. Server authorization remains mandatory.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { LegalDocumentData } from './types.js';
|
|
4
|
+
export type LegalDocumentProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> & {
|
|
5
|
+
document: LegalDocumentData;
|
|
6
|
+
};
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { cn } from '../../utils.js';
|
|
11
|
+
import { safeLegalHref } from './links.js';
|
|
12
|
+
let { document, class: className, ...rest }: LegalDocumentProps = $props();
|
|
13
|
+
const uid = $props.id();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<article {...rest} class={cn('mx-auto max-w-3xl px-6 py-10 text-sm leading-7 text-ink', className)}>
|
|
17
|
+
<header class="mb-8">
|
|
18
|
+
<h1 class="m-0 text-3xl font-semibold tracking-tight">{document.title}</h1>
|
|
19
|
+
{#if document.updated}
|
|
20
|
+
<p class="mt-3 text-muted">
|
|
21
|
+
Last updated: <time datetime={document.updated}>{document.updated}</time>
|
|
22
|
+
</p>
|
|
23
|
+
{/if}
|
|
24
|
+
</header>
|
|
25
|
+
<div class="space-y-8">
|
|
26
|
+
{#each document.sections as section (section.id)}
|
|
27
|
+
<section aria-labelledby={`${uid}-${section.id}`}>
|
|
28
|
+
<h2 id={`${uid}-${section.id}`} class="mb-3 text-lg font-semibold">{section.title}</h2>
|
|
29
|
+
<div class="space-y-3">
|
|
30
|
+
{#each section.paragraphs ?? [] as paragraph, index (index)}<p>{paragraph}</p>{/each}
|
|
31
|
+
{#if section.address?.length}
|
|
32
|
+
<address class="not-italic">
|
|
33
|
+
{#each section.address as line, index (index)}{#if index}<br />{/if}{line}{/each}
|
|
34
|
+
</address>
|
|
35
|
+
{/if}
|
|
36
|
+
{#if section.items?.length}
|
|
37
|
+
<ul class="list-disc space-y-2 pl-5">
|
|
38
|
+
{#each section.items as item, index (index)}<li>{item}</li>{/each}
|
|
39
|
+
</ul>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if section.links?.length}
|
|
42
|
+
<ul class="list-none space-y-2 p-0">
|
|
43
|
+
{#each section.links as link, index (index)}
|
|
44
|
+
<li>
|
|
45
|
+
{#if safeLegalHref(link.href, true)}<a
|
|
46
|
+
href={safeLegalHref(link.href, true)}
|
|
47
|
+
class="rounded underline underline-offset-4 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-focus"
|
|
48
|
+
>{link.label}</a
|
|
49
|
+
>{:else}{link.label}{/if}
|
|
50
|
+
</li>
|
|
51
|
+
{/each}
|
|
52
|
+
</ul>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
|
+
</section>
|
|
56
|
+
{/each}
|
|
57
|
+
</div>
|
|
58
|
+
</article>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import type { LegalDocumentData } from './types.js';
|
|
3
|
+
export type LegalDocumentProps = Omit<HTMLAttributes<HTMLElement>, 'children' | 'title'> & {
|
|
4
|
+
document: LegalDocumentData;
|
|
5
|
+
};
|
|
6
|
+
declare const LegalDocument: import("svelte").Component<LegalDocumentProps, {}, "">;
|
|
7
|
+
type LegalDocument = ReturnType<typeof LegalDocument>;
|
|
8
|
+
export default LegalDocument;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { LegalFooterLinks } from './types.js';
|
|
5
|
+
|
|
6
|
+
export type LegalFooterProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
7
|
+
links?: LegalFooterLinks;
|
|
8
|
+
/** App-owned contextual report action; replaces links.report when supplied. */
|
|
9
|
+
reportAction?: Snippet;
|
|
10
|
+
};
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { cn } from '../../utils.js';
|
|
15
|
+
import { legalFooterItems } from './links.js';
|
|
16
|
+
let { links = {}, reportAction, class: className, ...rest }: LegalFooterProps = $props();
|
|
17
|
+
const showReportAction = $derived(links.report !== false && !!reportAction);
|
|
18
|
+
const items = $derived(legalFooterItems(showReportAction ? { ...links, report: false } : links));
|
|
19
|
+
const linkClass =
|
|
20
|
+
'rounded text-inherit underline-offset-4 hover:text-ink hover:underline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-focus';
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if items.length || showReportAction}
|
|
24
|
+
<footer {...rest} class={cn('py-8 text-xs text-muted', className)}>
|
|
25
|
+
<nav aria-label="Legal and contact">
|
|
26
|
+
<ul class="m-0 flex list-none flex-wrap items-center justify-center gap-x-5 gap-y-3 p-0">
|
|
27
|
+
{#each items as item, index (index)}
|
|
28
|
+
<li><a href={item.href} class={linkClass}>{item.label}</a></li>
|
|
29
|
+
{/each}
|
|
30
|
+
{#if showReportAction}<li>{@render reportAction?.()}</li>{/if}
|
|
31
|
+
</ul>
|
|
32
|
+
</nav>
|
|
33
|
+
</footer>
|
|
34
|
+
{/if}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import type { LegalFooterLinks } from './types.js';
|
|
4
|
+
export type LegalFooterProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
|
5
|
+
links?: LegalFooterLinks;
|
|
6
|
+
/** App-owned contextual report action; replaces links.report when supplied. */
|
|
7
|
+
reportAction?: Snippet;
|
|
8
|
+
};
|
|
9
|
+
declare const LegalFooter: import("svelte").Component<LegalFooterProps, {}, "">;
|
|
10
|
+
type LegalFooter = ReturnType<typeof LegalFooter>;
|
|
11
|
+
export default LegalFooter;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LegalDocumentData, LegalDocumentKind, LegalDraftConfig } from './types.js';
|
|
2
|
+
/** Completeness checks only, never legal approval. Run during authoring/review. */
|
|
3
|
+
export declare function getLegalDraftIssues(config: LegalDraftConfig): string[];
|
|
4
|
+
/** Publication completeness only. A passing check is not legal approval. */
|
|
5
|
+
export declare function getLegalDocumentIssues(document: LegalDocumentData): string[];
|
|
6
|
+
/** Starter text, not legal advice. Save the result in the app and review it.
|
|
7
|
+
* Never use this as a runtime policy generator: a package update must not silently
|
|
8
|
+
* change published commitments. No jurisdiction, age, retention, or legal basis
|
|
9
|
+
* is inferred. Every generated document is a draft with no publication date. */
|
|
10
|
+
export declare function createLegalDrafts(config: LegalDraftConfig): Record<LegalDocumentKind, LegalDocumentData>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { safeLegalHref } from './links.js';
|
|
2
|
+
/** Completeness checks only, never legal approval. Run during authoring/review. */
|
|
3
|
+
export function getLegalDraftIssues(config) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
if (!config.app.name.trim())
|
|
6
|
+
issues.push('App name is missing.');
|
|
7
|
+
if (!safeLegalHref(config.app.url)?.startsWith('http'))
|
|
8
|
+
issues.push('A valid public app URL is required.');
|
|
9
|
+
if (!config.app.description.trim())
|
|
10
|
+
issues.push('Service description is missing.');
|
|
11
|
+
if (!config.operator.name.trim())
|
|
12
|
+
issues.push('Operator name is missing.');
|
|
13
|
+
if (!config.operator.address.length || config.operator.address.some((line) => !line.trim())) {
|
|
14
|
+
issues.push('The operator postal address must be supplied and reviewed.');
|
|
15
|
+
}
|
|
16
|
+
if (!safeLegalHref(`mailto:${config.operator.email}`, true))
|
|
17
|
+
issues.push('A valid operator contact email is required.');
|
|
18
|
+
if (!config.privacy.length)
|
|
19
|
+
issues.push('Describe actual data processing, recipients, storage, retention, transfers, and legal bases.');
|
|
20
|
+
if (!config.moderation.length)
|
|
21
|
+
issues.push('Describe actual reporting, moderation, and review procedures.');
|
|
22
|
+
return issues;
|
|
23
|
+
}
|
|
24
|
+
/** Publication completeness only. A passing check is not legal approval. */
|
|
25
|
+
export function getLegalDocumentIssues(document) {
|
|
26
|
+
const issues = [];
|
|
27
|
+
if (!document.title.trim())
|
|
28
|
+
issues.push('Document title is missing.');
|
|
29
|
+
if (document.status !== 'published')
|
|
30
|
+
issues.push('Document is still a draft.');
|
|
31
|
+
if (!document.updated ||
|
|
32
|
+
!/^\d{4}-\d{2}-\d{2}$/.test(document.updated) ||
|
|
33
|
+
!Number.isFinite(Date.parse(document.updated)) ||
|
|
34
|
+
new Date(document.updated).toISOString().slice(0, 10) !== document.updated) {
|
|
35
|
+
issues.push('A valid app-reviewed revision date is required.');
|
|
36
|
+
}
|
|
37
|
+
if (!document.sections.length)
|
|
38
|
+
issues.push('Document sections are missing.');
|
|
39
|
+
const ids = new Set();
|
|
40
|
+
for (const section of document.sections) {
|
|
41
|
+
if (!section.id || ids.has(section.id))
|
|
42
|
+
issues.push('Section IDs must be nonempty and unique.');
|
|
43
|
+
ids.add(section.id);
|
|
44
|
+
if (!section.title.trim())
|
|
45
|
+
issues.push('Section title is missing.');
|
|
46
|
+
}
|
|
47
|
+
return issues;
|
|
48
|
+
}
|
|
49
|
+
/** Starter text, not legal advice. Save the result in the app and review it.
|
|
50
|
+
* Never use this as a runtime policy generator: a package update must not silently
|
|
51
|
+
* change published commitments. No jurisdiction, age, retention, or legal basis
|
|
52
|
+
* is inferred. Every generated document is a draft with no publication date. */
|
|
53
|
+
export function createLegalDrafts(config) {
|
|
54
|
+
const { app, operator } = config;
|
|
55
|
+
const contact = {
|
|
56
|
+
id: 'contact',
|
|
57
|
+
title: 'Contact',
|
|
58
|
+
links: [{ label: operator.email, href: `mailto:${operator.email}` }]
|
|
59
|
+
};
|
|
60
|
+
const owner = {
|
|
61
|
+
id: 'operator',
|
|
62
|
+
title: 'Operator',
|
|
63
|
+
address: [operator.name, ...operator.address],
|
|
64
|
+
links: [{ label: app.name, href: app.url }]
|
|
65
|
+
};
|
|
66
|
+
const draft = (title, sections) => ({
|
|
67
|
+
title,
|
|
68
|
+
status: 'draft',
|
|
69
|
+
sections: structuredClone(sections)
|
|
70
|
+
});
|
|
71
|
+
// Copy per document, so editing one never changes another or the source config.
|
|
72
|
+
return {
|
|
73
|
+
terms: draft('Terms of service', [
|
|
74
|
+
{
|
|
75
|
+
id: 'service',
|
|
76
|
+
title: 'About the service',
|
|
77
|
+
paragraphs: [`${app.name} is ${app.description}.`]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'account',
|
|
81
|
+
title: 'Your account',
|
|
82
|
+
paragraphs: [
|
|
83
|
+
'Keep your account secure and only grant permissions you are comfortable with. Your account provider has its own terms.'
|
|
84
|
+
]
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'content',
|
|
88
|
+
title: 'Your content',
|
|
89
|
+
paragraphs: [
|
|
90
|
+
'You own your content and must have the right to publish it. Public AT Protocol records can be copied by other services. Hiding content here does not remove those copies.'
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'conduct',
|
|
95
|
+
title: 'Acceptable use',
|
|
96
|
+
items: [
|
|
97
|
+
'Do not publish illegal content or violate the rights of others.',
|
|
98
|
+
'Do not use the service for harassment, threats, scams, spam, or malicious software.',
|
|
99
|
+
'Do not disrupt the service or bypass access restrictions.'
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'availability',
|
|
104
|
+
title: 'Availability and ending use',
|
|
105
|
+
paragraphs: [
|
|
106
|
+
'The service may change or be unavailable. You can stop using it and revoke its permissions through your account provider. We may restrict access to address misuse or legal requirements. Your statutory rights remain unaffected.'
|
|
107
|
+
]
|
|
108
|
+
},
|
|
109
|
+
contact
|
|
110
|
+
]),
|
|
111
|
+
privacy: draft('Privacy policy', [
|
|
112
|
+
{
|
|
113
|
+
id: 'overview',
|
|
114
|
+
title: 'About this policy',
|
|
115
|
+
paragraphs: [
|
|
116
|
+
`This policy covers data handled by ${app.name}. Your account provider and other AT Protocol services have their own policies.`
|
|
117
|
+
]
|
|
118
|
+
},
|
|
119
|
+
owner,
|
|
120
|
+
...config.privacy,
|
|
121
|
+
{
|
|
122
|
+
id: 'rights',
|
|
123
|
+
title: 'Your choices and rights',
|
|
124
|
+
paragraphs: [
|
|
125
|
+
'Contact us to request access, correction, deletion, or restriction of your personal data, or to object to processing. You can also complain to a data-protection authority where applicable.',
|
|
126
|
+
'Delete public records or revoke app access through your account provider. We cannot remove copies held by other services.'
|
|
127
|
+
]
|
|
128
|
+
},
|
|
129
|
+
contact
|
|
130
|
+
]),
|
|
131
|
+
imprint: draft('Imprint', [owner, contact]),
|
|
132
|
+
moderation: draft('Moderation policy', [...config.moderation, contact])
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LegalFooterLinks, LegalLink } from './types.js';
|
|
2
|
+
/** Same-origin absolute paths and web URLs only. Never executable, credentialed,
|
|
3
|
+
* protocol-relative, or backslash-normalized URLs. Mail is allowed explicitly. */
|
|
4
|
+
export declare function safeLegalHref(value: string, allowMail?: boolean): string | undefined;
|
|
5
|
+
export declare function legalFooterItems(links: LegalFooterLinks): LegalLink[];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Same-origin absolute paths and web URLs only. Never executable, credentialed,
|
|
2
|
+
* protocol-relative, or backslash-normalized URLs. Mail is allowed explicitly. */
|
|
3
|
+
export function safeLegalHref(value, allowMail = false) {
|
|
4
|
+
if (!value ||
|
|
5
|
+
/\s/.test(value) ||
|
|
6
|
+
[...value].some((character) => character === '\\' || character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
|
|
7
|
+
return undefined;
|
|
8
|
+
if (value.startsWith('/') && !value.startsWith('//'))
|
|
9
|
+
return value;
|
|
10
|
+
if (allowMail && /^mailto:[^@<>?&#%]+@[^@<>?&#%]+\.[^@<>?&#%]+$/.test(value))
|
|
11
|
+
return value;
|
|
12
|
+
try {
|
|
13
|
+
const url = new URL(value);
|
|
14
|
+
if (['http:', 'https:'].includes(url.protocol) && !url.username && !url.password)
|
|
15
|
+
return url.href;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
/* Invalid URLs remain non-clickable. */
|
|
19
|
+
}
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const labels = {
|
|
23
|
+
terms: 'Terms',
|
|
24
|
+
privacy: 'Privacy',
|
|
25
|
+
imprint: 'Imprint',
|
|
26
|
+
moderation: 'Moderation',
|
|
27
|
+
report: 'Report',
|
|
28
|
+
source: 'Source'
|
|
29
|
+
};
|
|
30
|
+
export function legalFooterItems(links) {
|
|
31
|
+
return Object.keys(labels).flatMap((key) => {
|
|
32
|
+
const item = links[key];
|
|
33
|
+
if (!item)
|
|
34
|
+
return [];
|
|
35
|
+
const link = typeof item === 'string' ? { label: labels[key], href: item } : item;
|
|
36
|
+
const href = safeLegalHref(link.href, key === 'report');
|
|
37
|
+
return href ? [{ label: link.label, href }] : [];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type LegalDocumentKind = 'terms' | 'privacy' | 'imprint' | 'moderation';
|
|
2
|
+
export interface LegalLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
}
|
|
6
|
+
export interface LegalSection {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
paragraphs?: readonly string[];
|
|
10
|
+
items?: readonly string[];
|
|
11
|
+
address?: readonly string[];
|
|
12
|
+
links?: readonly LegalLink[];
|
|
13
|
+
}
|
|
14
|
+
/** App-owned, serializable text. Kit upgrades do not change a saved document. */
|
|
15
|
+
export interface LegalDocumentData {
|
|
16
|
+
title: string;
|
|
17
|
+
status: 'draft' | 'published';
|
|
18
|
+
/** The app's reviewed revision date, not a Kit release date. */
|
|
19
|
+
updated?: string;
|
|
20
|
+
sections: readonly LegalSection[];
|
|
21
|
+
}
|
|
22
|
+
/** Omit a link, or set it to false, to hide it. */
|
|
23
|
+
export type LegalFooterLink = string | false | LegalLink;
|
|
24
|
+
export type LegalFooterLinks = Partial<Record<LegalDocumentKind | 'report' | 'source', LegalFooterLink>>;
|
|
25
|
+
export interface LegalDraftConfig {
|
|
26
|
+
app: {
|
|
27
|
+
name: string;
|
|
28
|
+
url: string;
|
|
29
|
+
description: string;
|
|
30
|
+
};
|
|
31
|
+
operator: {
|
|
32
|
+
name: string;
|
|
33
|
+
email: string;
|
|
34
|
+
address: readonly string[];
|
|
35
|
+
};
|
|
36
|
+
/** Explicitly describe deployed behavior; templates infer no legal basis. */
|
|
37
|
+
privacy: readonly LegalSection[];
|
|
38
|
+
moderation: readonly LegalSection[];
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
searchActors,
|
|
17
17
|
onSelect,
|
|
18
18
|
label = 'Your handle',
|
|
19
|
-
placeholder = '
|
|
19
|
+
placeholder = 'Handle',
|
|
20
20
|
id,
|
|
21
21
|
name = 'handle',
|
|
22
22
|
disabled = false,
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
let dismissedQuery = '';
|
|
52
52
|
const message = $derived(
|
|
53
53
|
status === 'error'
|
|
54
|
-
? 'Search unavailable. Enter your full handle to continue.'
|
|
54
|
+
? 'Search unavailable. Enter your full handle or DID to continue.'
|
|
55
55
|
: status === 'ready' && !results.length
|
|
56
|
-
? 'No suggestions. You can still enter a full handle.'
|
|
56
|
+
? 'No suggestions. You can still enter a full handle or DID.'
|
|
57
57
|
: ''
|
|
58
58
|
);
|
|
59
59
|
const announcement = $derived(
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
$effect(() => {
|
|
68
68
|
const query = value.trim().replace(/^@+/, '');
|
|
69
69
|
const provider = searchActors;
|
|
70
|
-
|
|
70
|
+
// DIDs go directly to the app's login adapter, never to handle typeahead.
|
|
71
|
+
if (disabled || !provider || query.length < 2 || /^did:/i.test(query)) {
|
|
71
72
|
results = [];
|
|
72
73
|
status = 'idle';
|
|
73
74
|
open = false;
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
import graze from './brands/graze.svg?url';
|
|
5
5
|
import leaflet from './brands/leaflet.svg?url';
|
|
6
6
|
import tangled from './brands/tangled.svg?url';
|
|
7
|
-
import
|
|
7
|
+
import muSocial from './brands/mu-social.svg?url';
|
|
8
8
|
import surf from './brands/surf.svg?url';
|
|
9
9
|
import pckt from './brands/pckt.svg?url';
|
|
10
|
-
const apps = [bluesky, blacksky, graze, leaflet, tangled,
|
|
10
|
+
const apps = [bluesky, blacksky, graze, leaflet, tangled, muSocial, surf, pckt];
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
<!-- Existing brand marks, not replacement UI icons. Static: no endless decorative motion. -->
|
|
14
14
|
<div class="flex items-center justify-between gap-2 py-2" aria-hidden="true">
|
|
15
15
|
{#each apps as src (src)}<span
|
|
16
16
|
class="size-5 shrink-0 bg-muted [mask-size:contain] [mask-position:center] [mask-repeat:no-repeat] opacity-70 sm:size-6"
|
|
17
|
+
class:scale-125={src === muSocial}
|
|
17
18
|
style:mask-image={`url("${src}")`}
|
|
18
19
|
></span>{/each}
|
|
19
20
|
</div>
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
);
|
|
37
37
|
const dialogDescription = $derived(
|
|
38
38
|
step === 'permissions'
|
|
39
|
-
? 'Choose what this app can access.
|
|
39
|
+
? 'Choose what this app can access.'
|
|
40
40
|
: step === 'signup'
|
|
41
41
|
? 'You can use a Bluesky, Blacksky, or other Atmosphere account here. Otherwise, continue to create one.'
|
|
42
42
|
: description
|
|
@@ -52,9 +52,9 @@
|
|
|
52
52
|
document.getElementById(uid)?.querySelector<HTMLElement>('h2')?.focus();
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
function completed(
|
|
55
|
+
function completed(identifier: string, actor?: ActorSummary) {
|
|
56
56
|
if (closeOnSuccess) open = false;
|
|
57
|
-
onSuccess?.(
|
|
57
|
+
onSuccess?.(identifier, actor);
|
|
58
58
|
}
|
|
59
59
|
</script>
|
|
60
60
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
login: LoginCallback;
|
|
18
18
|
signup?: () => LoginResult | Promise<LoginResult>;
|
|
19
19
|
searchActors?: ActorSearch;
|
|
20
|
+
/** Initial handle or DID; retained under this name for compatibility. */
|
|
20
21
|
initialHandle?: string;
|
|
21
22
|
loginOnSelect?: boolean;
|
|
22
23
|
recentAccounts?: ActorSummary[];
|
|
@@ -28,7 +29,7 @@
|
|
|
28
29
|
description?: string;
|
|
29
30
|
showHeader?: boolean;
|
|
30
31
|
showApps?: boolean;
|
|
31
|
-
onSuccess?: (
|
|
32
|
+
onSuccess?: (identifier: string, actor?: ActorSummary) => void;
|
|
32
33
|
onStepChange?: (step: LoginStep) => void;
|
|
33
34
|
children?: Snippet;
|
|
34
35
|
footer?: Snippet<[LoginStep]>;
|
|
@@ -38,20 +39,20 @@
|
|
|
38
39
|
|
|
39
40
|
<script lang="ts">
|
|
40
41
|
import { onMount, onDestroy, tick, untrack } from 'svelte';
|
|
41
|
-
import { X,
|
|
42
|
+
import { X, TriangleAlert, ArrowRight } from '@sketchyicons/svelte';
|
|
42
43
|
import Button from '../Button.svelte';
|
|
43
44
|
import IconButton from '../IconButton.svelte';
|
|
44
45
|
import Icon from '../Icon.svelte';
|
|
45
46
|
import Avatar from '../Avatar.svelte';
|
|
46
|
-
import
|
|
47
|
+
import PermissionFields from './PermissionFields.svelte';
|
|
47
48
|
import Label from '../Label.svelte';
|
|
48
49
|
import Heading from '../Heading.svelte';
|
|
49
50
|
import ActorSearchField from './ActorSearchField.svelte';
|
|
50
51
|
import RecentAccounts from './RecentAccounts.svelte';
|
|
51
52
|
import AtmosphereApps from './AtmosphereApps.svelte';
|
|
52
53
|
import { actorName } from './types.js';
|
|
53
|
-
import {
|
|
54
|
-
import {
|
|
54
|
+
import { identifierError, normalizeHandle, normalizeIdentifier } from './handle.js';
|
|
55
|
+
import { resolveLoginOptions } from './permissions.js';
|
|
55
56
|
import {
|
|
56
57
|
loadRecentAccounts,
|
|
57
58
|
writeRecentAccounts,
|
|
@@ -89,7 +90,7 @@
|
|
|
89
90
|
let error = $state('');
|
|
90
91
|
let useRecent = $state(true);
|
|
91
92
|
let choices = $state<Record<string, boolean>>({});
|
|
92
|
-
let pending = $state<{
|
|
93
|
+
let pending = $state<{ identifier: string; actor?: ActorSummary }>();
|
|
93
94
|
let input = $state<HTMLInputElement | null>(null);
|
|
94
95
|
let submitRef = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
|
|
95
96
|
let heading = $state<HTMLHeadingElement | null>(null);
|
|
@@ -108,7 +109,7 @@
|
|
|
108
109
|
);
|
|
109
110
|
const descriptionText = $derived(
|
|
110
111
|
step === 'permissions'
|
|
111
|
-
? 'Choose what this app can access.
|
|
112
|
+
? 'Choose what this app can access.'
|
|
112
113
|
: step === 'signup'
|
|
113
114
|
? 'You can use a Bluesky, Blacksky, or other Atmosphere account here. Otherwise, continue to create one.'
|
|
114
115
|
: description
|
|
@@ -122,7 +123,7 @@
|
|
|
122
123
|
if (next !== previous) {
|
|
123
124
|
previous = next;
|
|
124
125
|
error = '';
|
|
125
|
-
if (selected && normalizeHandle(selected.handle) !==
|
|
126
|
+
if (selected && normalizeHandle(selected.handle) !== normalizeIdentifier(next))
|
|
126
127
|
selected = undefined;
|
|
127
128
|
}
|
|
128
129
|
});
|
|
@@ -153,32 +154,32 @@
|
|
|
153
154
|
if (next === 'login') await focusAccount();
|
|
154
155
|
else if (showHeader) heading?.focus();
|
|
155
156
|
}
|
|
156
|
-
async function authenticate(request: {
|
|
157
|
+
async function authenticate(request: { identifier: string; actor?: ActorSummary }) {
|
|
157
158
|
if (busy) return;
|
|
158
159
|
busy = true;
|
|
159
160
|
error = '';
|
|
160
161
|
try {
|
|
161
162
|
const result = permissions.length
|
|
162
163
|
? await login(
|
|
163
|
-
request.
|
|
164
|
+
request.identifier,
|
|
164
165
|
request.actor,
|
|
165
166
|
resolveLoginOptions(permissions, choices, baseScopes)
|
|
166
167
|
)
|
|
167
|
-
: await login(request.
|
|
168
|
+
: await login(request.identifier, request.actor);
|
|
168
169
|
if (!alive) return;
|
|
169
170
|
if (result === false) return;
|
|
170
171
|
if (request.actor?.did) {
|
|
171
172
|
const next = [
|
|
172
173
|
{ ...request.actor, lastUsedAt: new Date().toISOString() },
|
|
173
174
|
...recentAccounts.filter(
|
|
174
|
-
(actor) => actor.did !== request.actor?.did && actor.handle !== request.
|
|
175
|
+
(actor) => actor.did !== request.actor?.did && actor.handle !== request.identifier
|
|
175
176
|
)
|
|
176
177
|
].slice(0, recentLimit);
|
|
177
178
|
recentAccounts = persistRecentAccounts
|
|
178
179
|
? writeRecentAccounts(next, recentAccountOptions)
|
|
179
180
|
: next;
|
|
180
181
|
}
|
|
181
|
-
onSuccess?.(request.
|
|
182
|
+
onSuccess?.(request.identifier, request.actor);
|
|
182
183
|
} catch (caught) {
|
|
183
184
|
if (alive)
|
|
184
185
|
error = caught instanceof Error ? caught.message : 'Login failed. Please try again.';
|
|
@@ -190,10 +191,10 @@
|
|
|
190
191
|
}
|
|
191
192
|
}
|
|
192
193
|
}
|
|
193
|
-
async function beginLogin(
|
|
194
|
+
async function beginLogin(identifier = value, actor = selected) {
|
|
194
195
|
if (busy) return;
|
|
195
|
-
const normalized =
|
|
196
|
-
const problem =
|
|
196
|
+
const normalized = normalizeIdentifier(identifier);
|
|
197
|
+
const problem = identifierError(normalized);
|
|
197
198
|
if (problem) {
|
|
198
199
|
error = problem;
|
|
199
200
|
await focusAccount();
|
|
@@ -202,7 +203,7 @@
|
|
|
202
203
|
previous = normalized;
|
|
203
204
|
value = normalized;
|
|
204
205
|
const request = {
|
|
205
|
-
|
|
206
|
+
identifier: normalized,
|
|
206
207
|
actor: actor && normalizeHandle(actor.handle) === normalized ? actor : undefined
|
|
207
208
|
};
|
|
208
209
|
pending = request;
|
|
@@ -285,7 +286,7 @@
|
|
|
285
286
|
onclick={() => {
|
|
286
287
|
useRecent = false;
|
|
287
288
|
void focusAccount();
|
|
288
|
-
}}>Use another
|
|
289
|
+
}}>Use another account</Button
|
|
289
290
|
>
|
|
290
291
|
{:else}
|
|
291
292
|
{#if selected}<div
|
|
@@ -345,41 +346,17 @@
|
|
|
345
346
|
>
|
|
346
347
|
</div>{/if}
|
|
347
348
|
{:else if step === 'permissions'}
|
|
348
|
-
{#if pending}<
|
|
349
|
+
{#if pending}<p class="flex min-w-0 items-center gap-2 text-xs text-muted">
|
|
350
|
+
<span class="shrink-0">Logging in as</span>
|
|
349
351
|
{#if pending.actor}<Avatar
|
|
350
352
|
name={actorName(pending.actor)}
|
|
351
353
|
src={pending.actor.avatar}
|
|
352
|
-
size={
|
|
354
|
+
size={20}
|
|
353
355
|
aria-hidden="true"
|
|
354
356
|
/>{/if}
|
|
355
|
-
<
|
|
356
|
-
</
|
|
357
|
-
<
|
|
358
|
-
<legend class="sr-only">Requested permissions</legend>
|
|
359
|
-
{#each permissions as permission, index (permission.id)}<div class="flex items-start gap-3">
|
|
360
|
-
<Icon img={permission.icon ?? ShieldCheck} size={20} class="mt-1 shrink-0 text-muted" />
|
|
361
|
-
<div class="min-w-0 flex-1 space-y-1.5">
|
|
362
|
-
<Label for={`${uid}-permission-${index}`} class="leading-snug"
|
|
363
|
-
>{permission.label}{permission.required ? ' (required)' : ''}</Label
|
|
364
|
-
>
|
|
365
|
-
<p
|
|
366
|
-
id={`${uid}-permission-${index}-description`}
|
|
367
|
-
class="text-xs leading-relaxed text-muted"
|
|
368
|
-
>
|
|
369
|
-
{permission.description}
|
|
370
|
-
</p>
|
|
371
|
-
</div>
|
|
372
|
-
<Checkbox
|
|
373
|
-
id={`${uid}-permission-${index}`}
|
|
374
|
-
class="mt-1"
|
|
375
|
-
checked={isPermissionSelected(permission, choices)}
|
|
376
|
-
disabled={busy || permission.required}
|
|
377
|
-
aria-describedby={`${uid}-permission-${index}-description`}
|
|
378
|
-
onchange={(event) =>
|
|
379
|
-
(choices = { ...choices, [permission.id]: event.currentTarget.checked })}
|
|
380
|
-
/>
|
|
381
|
-
</div>{/each}
|
|
382
|
-
</fieldset>
|
|
357
|
+
<span class="min-w-0 break-words">{pending.identifier}</span>
|
|
358
|
+
</p>{/if}
|
|
359
|
+
<PermissionFields {permissions} bind:choices disabled={busy} />
|
|
383
360
|
<Button type="submit" bind:ref={submitRef} loading={busy} class="w-full"
|
|
384
361
|
>{busy ? 'Logging in…' : 'Continue to log in'}</Button
|
|
385
362
|
><Button variant="secondary" disabled={busy} class="w-full" onclick={() => changeStep('login')}
|
|
@@ -6,6 +6,7 @@ export type LoginFormProps = Omit<HTMLFormAttributes, 'children' | 'onsubmit' |
|
|
|
6
6
|
login: LoginCallback;
|
|
7
7
|
signup?: () => LoginResult | Promise<LoginResult>;
|
|
8
8
|
searchActors?: ActorSearch;
|
|
9
|
+
/** Initial handle or DID; retained under this name for compatibility. */
|
|
9
10
|
initialHandle?: string;
|
|
10
11
|
loginOnSelect?: boolean;
|
|
11
12
|
recentAccounts?: ActorSummary[];
|
|
@@ -17,7 +18,7 @@ export type LoginFormProps = Omit<HTMLFormAttributes, 'children' | 'onsubmit' |
|
|
|
17
18
|
description?: string;
|
|
18
19
|
showHeader?: boolean;
|
|
19
20
|
showApps?: boolean;
|
|
20
|
-
onSuccess?: (
|
|
21
|
+
onSuccess?: (identifier: string, actor?: ActorSummary) => void;
|
|
21
22
|
onStepChange?: (step: LoginStep) => void;
|
|
22
23
|
children?: Snippet;
|
|
23
24
|
footer?: Snippet<[LoginStep]>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { ShieldCheck } from '@sketchyicons/svelte';
|
|
3
|
+
import Checkbox from '../Checkbox.svelte';
|
|
4
|
+
import Icon from '../Icon.svelte';
|
|
5
|
+
import Label from '../Label.svelte';
|
|
6
|
+
import { cn } from '../../utils.js';
|
|
7
|
+
import { isPermissionSelected } from './permissions.js';
|
|
8
|
+
import type { LoginPermission } from './types.js';
|
|
9
|
+
|
|
10
|
+
let {
|
|
11
|
+
permissions,
|
|
12
|
+
choices = $bindable({}),
|
|
13
|
+
disabled = false
|
|
14
|
+
}: {
|
|
15
|
+
permissions: readonly LoginPermission[];
|
|
16
|
+
choices?: Record<string, boolean>;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
} = $props();
|
|
19
|
+
const uid = $props.id();
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<fieldset class="grid min-w-0 gap-5 border-0 py-1" {disabled}>
|
|
23
|
+
<legend class="sr-only">Requested permissions</legend>
|
|
24
|
+
{#each permissions as permission, index (permission.id)}
|
|
25
|
+
<div class="flex items-start gap-3">
|
|
26
|
+
<Icon img={permission.icon ?? ShieldCheck} size={20} class="mt-1 shrink-0 text-muted" />
|
|
27
|
+
<div class="min-w-0 flex-1 space-y-1.5">
|
|
28
|
+
<Label
|
|
29
|
+
id={`${uid}-${index}-label`}
|
|
30
|
+
for={permission.required ? undefined : `${uid}-${index}`}
|
|
31
|
+
class={cn('leading-snug', permission.required && 'cursor-default')}
|
|
32
|
+
>
|
|
33
|
+
{permission.label}{permission.required ? ' (required)' : ''}
|
|
34
|
+
</Label>
|
|
35
|
+
<p id={`${uid}-${index}-description`} class="text-xs leading-relaxed text-muted">
|
|
36
|
+
{permission.description}
|
|
37
|
+
</p>
|
|
38
|
+
</div>
|
|
39
|
+
<Checkbox
|
|
40
|
+
id={`${uid}-${index}`}
|
|
41
|
+
class={cn('mt-1', permission.required && 'pointer-events-none')}
|
|
42
|
+
checked={isPermissionSelected(permission, choices)}
|
|
43
|
+
{disabled}
|
|
44
|
+
aria-disabled={permission.required || undefined}
|
|
45
|
+
aria-labelledby={`${uid}-${index}-label`}
|
|
46
|
+
aria-describedby={`${uid}-${index}-description`}
|
|
47
|
+
tabindex={permission.required ? -1 : undefined}
|
|
48
|
+
onclick={(event) => {
|
|
49
|
+
if (permission.required) event.preventDefault();
|
|
50
|
+
}}
|
|
51
|
+
onchange={(event) =>
|
|
52
|
+
(choices = { ...choices, [permission.id]: event.currentTarget.checked })}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
{/each}
|
|
56
|
+
</fieldset>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LoginPermission } from './types.js';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
permissions: readonly LoginPermission[];
|
|
4
|
+
choices?: Record<string, boolean>;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const PermissionFields: import("svelte").Component<$$ComponentProps, {}, "choices">;
|
|
8
|
+
type PermissionFields = ReturnType<typeof PermissionFields>;
|
|
9
|
+
export default PermissionFields;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { LoginOptions, LoginPermission, LoginResult } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type SignupPermissionsProps = {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
permissions: readonly LoginPermission[];
|
|
7
|
+
baseScopes?: readonly string[];
|
|
8
|
+
signup: (options: LoginOptions) => LoginResult | Promise<LoginResult>;
|
|
9
|
+
};
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
import Modal from '../Modal.svelte';
|
|
14
|
+
import Button from '../Button.svelte';
|
|
15
|
+
import PermissionFields from './PermissionFields.svelte';
|
|
16
|
+
import { resolveLoginOptions } from './permissions.js';
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
open = $bindable(false),
|
|
20
|
+
permissions,
|
|
21
|
+
baseScopes = ['atproto'],
|
|
22
|
+
signup
|
|
23
|
+
}: SignupPermissionsProps = $props();
|
|
24
|
+
let choices = $state<Record<string, boolean>>({});
|
|
25
|
+
let busy = $state(false);
|
|
26
|
+
let error = $state('');
|
|
27
|
+
|
|
28
|
+
async function continueSignup() {
|
|
29
|
+
if (busy) return;
|
|
30
|
+
busy = true;
|
|
31
|
+
error = '';
|
|
32
|
+
try {
|
|
33
|
+
const result = await signup(resolveLoginOptions(permissions, choices, baseScopes));
|
|
34
|
+
if (result !== false) open = false;
|
|
35
|
+
} catch (caught) {
|
|
36
|
+
error = caught instanceof Error ? caught.message : 'Signup failed. Please try again.';
|
|
37
|
+
} finally {
|
|
38
|
+
busy = false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<Modal bind:open title="Requested permissions" dismissible={!busy}>
|
|
44
|
+
<div class="space-y-5">
|
|
45
|
+
<PermissionFields {permissions} bind:choices disabled={busy} />
|
|
46
|
+
{#if error}<p role="alert" class="text-sm">{error}</p>{/if}
|
|
47
|
+
<Button class="w-full" disabled={busy} onclick={continueSignup}>
|
|
48
|
+
{busy ? 'Continuing…' : 'Continue'}
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
</Modal>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LoginOptions, LoginPermission, LoginResult } from './types.js';
|
|
2
|
+
export type SignupPermissionsProps = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
permissions: readonly LoginPermission[];
|
|
5
|
+
baseScopes?: readonly string[];
|
|
6
|
+
signup: (options: LoginOptions) => LoginResult | Promise<LoginResult>;
|
|
7
|
+
};
|
|
8
|
+
declare const SignupPermissions: import("svelte").Component<SignupPermissionsProps, {}, "open">;
|
|
9
|
+
type SignupPermissions = ReturnType<typeof SignupPermissions>;
|
|
10
|
+
export default SignupPermissions;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Atmosphere brand marks
|
|
2
2
|
|
|
3
|
-
Bluesky, Blacksky, Graze, Leaflet, Tangled,
|
|
3
|
+
Bluesky, Blacksky, Graze, Leaflet, Tangled, Surf, and Pckt marks were copied from Florian’s existing Atmo UI login assets (`11-atmo-ui/codebase/src/lib/login/icons/`). The login implementation is covered by the package's root MIT `LICENSE`, which retains the original copyright notice.
|
|
4
|
+
|
|
5
|
+
The mu.social mark comes from the supplied Eurosky brand package: `branding/mu-brand-package/logos/svg/mu-logos_mu_logo_base-01.svg`. Its geometry is unchanged.
|
|
4
6
|
|
|
5
7
|
Brand marks remain the property of their respective owners; the source-code license is not a grant of trademark rights. They are used only as decorative examples of compatible Atmosphere services, not to imply endorsement. Surf’s opaque background rectangle was removed so the existing mark can render transparently like its neighbors. The other geometry is unchanged.
|
|
6
8
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="1000" height="658.9381" viewBox="0 0 1000 658.9381">
|
|
3
|
+
<defs>
|
|
4
|
+
<style>
|
|
5
|
+
.cls-1 {
|
|
6
|
+
fill: #202020;
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
</defs>
|
|
10
|
+
<path class="cls-1" d="M584.2062,303.3777v198.5049l-119.4809,21.0622v-219.5671c0-17.6445-14.3654-32.0098-32.0098-32.0098s-32.0097,14.3653-32.0097,32.0098v230.8485l-119.4809,21.0615v-220.4836c0-17.6445-14.3378-32.0098-32.0098-32.0098-17.6442,0-31.982,14.3653-31.982,32.0098v231.7928l-119.4809,21.0622v-252.855c0-60.4073,35.5109-112.6456,86.7488-136.9586,19.6449-9.3087,41.5681-14.5322,64.7141-14.5322,4.7515,0,9.4196.2222,14.0598.639,20.673,1.9171,40.1512,8.0021,57.5732,17.3942,14.0598-15.3937,31.2039-27.8697,50.4876-36.4559,18.7557-8.3359,39.5398-13.0038,61.3798-13.0038,3.4733,0,6.9189.1113,10.3366.3612,78.7185,5.3073,141.1542,71.0771,141.1542,151.1295Z"/>
|
|
11
|
+
<path class="cls-1" d="M902.2478,71.279v251.1879c0,62.7691-38.3728,116.7579-92.9173,139.7091h-.0278c-18.0054,7.5859-37.7892,11.7817-58.5457,11.7817-2.3896,0-4.7792-.0556-7.1411-.1665h-.0833c-80.1913-3.7792-144.2664-70.2159-144.2664-151.3242V124.7119l119.4809-21.0897v218.8446c0,17.6442,14.3656,32.0098,32.0098,32.0098s32.0098-14.3656,32.0098-32.0098V92.3412l119.4809-21.0622Z"/>
|
|
12
|
+
</svg>
|
|
@@ -1,3 +1,8 @@
|
|
|
1
1
|
export declare const normalizeHandle: (value: string) => string;
|
|
2
2
|
export declare const isValidHandle: (value: string) => boolean;
|
|
3
3
|
export declare function handleError(value: string): string | undefined;
|
|
4
|
+
/** Normalize handles only; DID method-specific identifiers are case-sensitive. */
|
|
5
|
+
export declare function normalizeIdentifier(value: string): string;
|
|
6
|
+
/** Syntax validation only. The app's login adapter resolves the account. */
|
|
7
|
+
export declare function isValidIdentifier(value: string): boolean;
|
|
8
|
+
export declare function identifierError(value: string): string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isHandle } from '@atcute/lexicons/syntax';
|
|
1
|
+
import { isDid, isHandle } from '@atcute/lexicons/syntax';
|
|
2
2
|
export const normalizeHandle = (value) => value.trim().replace(/^@+/, '').toLowerCase();
|
|
3
3
|
export const isValidHandle = (value) => isHandle(normalizeHandle(value));
|
|
4
4
|
export function handleError(value) {
|
|
@@ -9,3 +9,21 @@ export function handleError(value) {
|
|
|
9
9
|
return 'Enter a valid handle, such as alice.bsky.social.';
|
|
10
10
|
return undefined;
|
|
11
11
|
}
|
|
12
|
+
/** Normalize handles only; DID method-specific identifiers are case-sensitive. */
|
|
13
|
+
export function normalizeIdentifier(value) {
|
|
14
|
+
const identifier = value.trim();
|
|
15
|
+
// Preserve malformed DID inputs too: never turn @did:… or DID:… into a valid DID.
|
|
16
|
+
return /^@*did:/i.test(identifier) ? identifier : normalizeHandle(identifier);
|
|
17
|
+
}
|
|
18
|
+
/** Syntax validation only. The app's login adapter resolves the account. */
|
|
19
|
+
export function isValidIdentifier(value) {
|
|
20
|
+
const identifier = normalizeIdentifier(value);
|
|
21
|
+
return isDid(identifier) || isHandle(identifier);
|
|
22
|
+
}
|
|
23
|
+
export function identifierError(value) {
|
|
24
|
+
if (!value.trim())
|
|
25
|
+
return 'Enter your handle or DID.';
|
|
26
|
+
if (!isValidIdentifier(value))
|
|
27
|
+
return 'Enter a valid handle or DID.';
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
@@ -23,5 +23,6 @@ export type LoginOptions = {
|
|
|
23
23
|
scopes: string[];
|
|
24
24
|
};
|
|
25
25
|
export type LoginResult = boolean | void;
|
|
26
|
-
|
|
26
|
+
/** Receives a normalized handle or a trimmed, case-preserved DID. Resolution belongs to the app. */
|
|
27
|
+
export type LoginCallback = (identifier: string, actor?: ActorSummary, options?: LoginOptions) => LoginResult | Promise<LoginResult>;
|
|
27
28
|
export declare const actorName: (actor: ActorSummary) => string;
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { ReportSubmit } from './types.js';
|
|
3
|
+
|
|
4
|
+
export interface ReportDialogProps {
|
|
5
|
+
open?: boolean;
|
|
6
|
+
isLoggedIn: boolean;
|
|
7
|
+
/** App-owned report mailbox, not the subject's email address. */
|
|
8
|
+
email: string;
|
|
9
|
+
/** Canonical event/account link for the email fallback. */
|
|
10
|
+
subjectUrl: string;
|
|
11
|
+
submit: ReportSubmit;
|
|
12
|
+
onsignin: () => void;
|
|
13
|
+
class?: string;
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script lang="ts">
|
|
18
|
+
import Button from '../Button.svelte';
|
|
19
|
+
import Modal from '../Modal.svelte';
|
|
20
|
+
import Textarea from '../Textarea.svelte';
|
|
21
|
+
import { cn } from '../../utils.js';
|
|
22
|
+
import { control } from '../../styles.js';
|
|
23
|
+
import { reportEmailHref } from './email.js';
|
|
24
|
+
import type { ReportReasonType } from './types.js';
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
open = $bindable(false),
|
|
28
|
+
isLoggedIn,
|
|
29
|
+
email,
|
|
30
|
+
subjectUrl,
|
|
31
|
+
submit,
|
|
32
|
+
onsignin,
|
|
33
|
+
class: className
|
|
34
|
+
}: ReportDialogProps = $props();
|
|
35
|
+
let reasonType = $state<ReportReasonType>('com.atproto.moderation.defs#reasonSpam');
|
|
36
|
+
let reason = $state('');
|
|
37
|
+
let pending = $state(false);
|
|
38
|
+
let message = $state('');
|
|
39
|
+
let sent = $state(false);
|
|
40
|
+
let needsSignIn = $state(false);
|
|
41
|
+
let generation = 0;
|
|
42
|
+
const id = $props.id();
|
|
43
|
+
const emailHref = $derived(reportEmailHref(email, subjectUrl, reason));
|
|
44
|
+
|
|
45
|
+
// Each opening/subject starts fresh. Ignore a late result from a closed or
|
|
46
|
+
// retargeted dialog rather than showing success for a different subject.
|
|
47
|
+
$effect(() => {
|
|
48
|
+
void open;
|
|
49
|
+
void subjectUrl;
|
|
50
|
+
generation += 1;
|
|
51
|
+
reasonType = 'com.atproto.moderation.defs#reasonSpam';
|
|
52
|
+
reason = '';
|
|
53
|
+
pending = false;
|
|
54
|
+
message = '';
|
|
55
|
+
sent = false;
|
|
56
|
+
needsSignIn = false;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function signIn() {
|
|
60
|
+
open = false;
|
|
61
|
+
onsignin();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function send(event: SubmitEvent) {
|
|
65
|
+
event.preventDefault();
|
|
66
|
+
if (pending || !isLoggedIn) return;
|
|
67
|
+
const current = ++generation;
|
|
68
|
+
pending = true;
|
|
69
|
+
message = '';
|
|
70
|
+
needsSignIn = false;
|
|
71
|
+
try {
|
|
72
|
+
const failure = await submit({
|
|
73
|
+
reasonType,
|
|
74
|
+
...(reason.trim() ? { reason: reason.trim() } : {})
|
|
75
|
+
});
|
|
76
|
+
if (current !== generation) return;
|
|
77
|
+
if (failure) {
|
|
78
|
+
message = failure.message;
|
|
79
|
+
needsSignIn = failure.needsSignIn ?? false;
|
|
80
|
+
} else {
|
|
81
|
+
sent = true;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
if (current === generation) {
|
|
85
|
+
message = 'Report could not be sent. Try again or report by email.';
|
|
86
|
+
}
|
|
87
|
+
} finally {
|
|
88
|
+
if (current === generation) pending = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
</script>
|
|
92
|
+
|
|
93
|
+
<Modal
|
|
94
|
+
bind:open
|
|
95
|
+
title="Report"
|
|
96
|
+
description={isLoggedIn ? 'Only the moderators can see your report.' : undefined}
|
|
97
|
+
dismissible={!pending}
|
|
98
|
+
class={cn('max-w-md', className)}
|
|
99
|
+
>
|
|
100
|
+
{#if !isLoggedIn}
|
|
101
|
+
<div class="space-y-4">
|
|
102
|
+
<p>
|
|
103
|
+
Sign in to report directly, or email
|
|
104
|
+
<a class="underline" href={emailHref}>{email}</a>. Please include a link to the event or
|
|
105
|
+
account and your reason for reporting.
|
|
106
|
+
</p>
|
|
107
|
+
<Button onclick={signIn}>Sign in</Button>
|
|
108
|
+
</div>
|
|
109
|
+
{:else if sent}
|
|
110
|
+
<p role="status" class="mb-4">Report sent.</p>
|
|
111
|
+
<Button onclick={() => (open = false)}>Done</Button>
|
|
112
|
+
{:else}
|
|
113
|
+
<form onsubmit={send} class="space-y-4">
|
|
114
|
+
<label for={`${id}-reason`} class="block text-sm">Reason</label>
|
|
115
|
+
<select id={`${id}-reason`} bind:value={reasonType} disabled={pending} class={control}>
|
|
116
|
+
<option value="com.atproto.moderation.defs#reasonSpam">Spam</option>
|
|
117
|
+
<option value="com.atproto.moderation.defs#reasonViolation"
|
|
118
|
+
>Harmful or illegal content</option
|
|
119
|
+
>
|
|
120
|
+
<option value="com.atproto.moderation.defs#reasonOther">Other</option>
|
|
121
|
+
</select>
|
|
122
|
+
<label for={`${id}-details`} class="block text-sm">Details (optional)</label>
|
|
123
|
+
<Textarea
|
|
124
|
+
id={`${id}-details`}
|
|
125
|
+
bind:value={reason}
|
|
126
|
+
maxlength={2000}
|
|
127
|
+
rows={4}
|
|
128
|
+
disabled={pending}
|
|
129
|
+
/>
|
|
130
|
+
{#if message}
|
|
131
|
+
<div class="space-y-2 text-sm">
|
|
132
|
+
<p role="alert">{message}</p>
|
|
133
|
+
<a class="underline" href={emailHref}>Email {email} instead</a>
|
|
134
|
+
{#if needsSignIn}<Button variant="ghost" onclick={signIn}>Sign in again</Button>{/if}
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
137
|
+
<Button type="submit" disabled={pending}>{pending ? 'Sending…' : 'Send report'}</Button>
|
|
138
|
+
</form>
|
|
139
|
+
{/if}
|
|
140
|
+
</Modal>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ReportSubmit } from './types.js';
|
|
2
|
+
export interface ReportDialogProps {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
isLoggedIn: boolean;
|
|
5
|
+
/** App-owned report mailbox, not the subject's email address. */
|
|
6
|
+
email: string;
|
|
7
|
+
/** Canonical event/account link for the email fallback. */
|
|
8
|
+
subjectUrl: string;
|
|
9
|
+
submit: ReportSubmit;
|
|
10
|
+
onsignin: () => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const ReportDialog: import("svelte").Component<ReportDialogProps, {}, "open">;
|
|
14
|
+
type ReportDialog = ReturnType<typeof ReportDialog>;
|
|
15
|
+
export default ReportDialog;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The app owns the destination and canonical subject URL. Encode details as
|
|
2
|
+
* body text, never additional mail headers. Use %20 rather than form-style +. */
|
|
3
|
+
export function reportEmailHref(email, subjectUrl, reason = '') {
|
|
4
|
+
const body = `Event or account: ${subjectUrl}\n\nReason: ${reason.trim()}`;
|
|
5
|
+
return `mailto:${email}?subject=Content%20report&body=${encodeURIComponent(body)}`;
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type ReportReasonType = 'com.atproto.moderation.defs#reasonSpam' | 'com.atproto.moderation.defs#reasonViolation' | 'com.atproto.moderation.defs#reasonOther';
|
|
2
|
+
export interface ReportInput {
|
|
3
|
+
reasonType: ReportReasonType;
|
|
4
|
+
reason?: string;
|
|
5
|
+
}
|
|
6
|
+
/** Return a user-facing failure from the app's submission adapter. */
|
|
7
|
+
export interface ReportFailure {
|
|
8
|
+
message: string;
|
|
9
|
+
needsSignIn?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export type ReportSubmit = (input: ReportInput) => Promise<void | ReportFailure>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,15 @@ export { default as Avatar } from './components/Avatar.svelte';
|
|
|
15
15
|
export { default as InsetImage } from './components/InsetImage.svelte';
|
|
16
16
|
export { default as Badge } from './components/Badge.svelte';
|
|
17
17
|
export { default as Modal } from './components/Modal.svelte';
|
|
18
|
+
export { default as LegalDocument } from './components/legal/LegalDocument.svelte';
|
|
19
|
+
export type { LegalDocumentProps } from './components/legal/LegalDocument.svelte';
|
|
20
|
+
export { default as LegalFooter } from './components/legal/LegalFooter.svelte';
|
|
21
|
+
export type { LegalFooterProps } from './components/legal/LegalFooter.svelte';
|
|
22
|
+
export { createLegalDrafts, getLegalDraftIssues, getLegalDocumentIssues } from './components/legal/drafts.js';
|
|
23
|
+
export type { LegalDocumentKind, LegalDocumentData, LegalSection, LegalLink, LegalFooterLink, LegalFooterLinks, LegalDraftConfig } from './components/legal/types.js';
|
|
24
|
+
export { default as ReportDialog } from './components/report/ReportDialog.svelte';
|
|
25
|
+
export type { ReportDialogProps } from './components/report/ReportDialog.svelte';
|
|
26
|
+
export type { ReportReasonType, ReportInput, ReportFailure, ReportSubmit } from './components/report/types.js';
|
|
18
27
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
19
28
|
export type { TabItem } from './components/Tabs.svelte';
|
|
20
29
|
export { default as Icon } from './components/Icon.svelte';
|
|
@@ -26,11 +35,13 @@ export { default as LoginForm } from './components/login/LoginForm.svelte';
|
|
|
26
35
|
export type { LoginFormProps } from './components/login/LoginForm.svelte';
|
|
27
36
|
export { default as LoginDialog } from './components/login/LoginDialog.svelte';
|
|
28
37
|
export type { LoginDialogProps } from './components/login/LoginDialog.svelte';
|
|
38
|
+
export { default as SignupPermissions } from './components/login/SignupPermissions.svelte';
|
|
39
|
+
export type { SignupPermissionsProps } from './components/login/SignupPermissions.svelte';
|
|
29
40
|
export { default as ActorSearchField } from './components/login/ActorSearchField.svelte';
|
|
30
41
|
export { default as RecentAccounts } from './components/login/RecentAccounts.svelte';
|
|
31
42
|
export type { ActorSummary, ActorSearch, LoginCallback, LoginResult, LoginPermission, LoginOptions, LoginStep } from './components/login/types.js';
|
|
32
43
|
export { createActorSearch } from './components/login/search.js';
|
|
33
|
-
export { normalizeHandle, isValidHandle } from './components/login/handle.js';
|
|
44
|
+
export { normalizeHandle, isValidHandle, normalizeIdentifier, isValidIdentifier } from './components/login/handle.js';
|
|
34
45
|
export { resolveLoginOptions, isPermissionSelected } from './components/login/permissions.js';
|
|
35
46
|
export { loadRecentAccounts, writeRecentAccounts, saveRecentAccount, removeRecentAccount, RECENT_ACCOUNTS_STORAGE_KEY } from './components/login/recent-accounts.js';
|
|
36
47
|
export type { RecentAccount, RecentAccountStorage, RecentAccountStorageOptions } from './components/login/recent-accounts.js';
|
package/dist/index.js
CHANGED
|
@@ -13,16 +13,21 @@ export { default as Avatar } from './components/Avatar.svelte';
|
|
|
13
13
|
export { default as InsetImage } from './components/InsetImage.svelte';
|
|
14
14
|
export { default as Badge } from './components/Badge.svelte';
|
|
15
15
|
export { default as Modal } from './components/Modal.svelte';
|
|
16
|
+
export { default as LegalDocument } from './components/legal/LegalDocument.svelte';
|
|
17
|
+
export { default as LegalFooter } from './components/legal/LegalFooter.svelte';
|
|
18
|
+
export { createLegalDrafts, getLegalDraftIssues, getLegalDocumentIssues } from './components/legal/drafts.js';
|
|
19
|
+
export { default as ReportDialog } from './components/report/ReportDialog.svelte';
|
|
16
20
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
17
21
|
export { default as Icon } from './components/Icon.svelte';
|
|
18
22
|
export { default as IconRail } from './components/IconRail.svelte';
|
|
19
23
|
export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
|
|
20
24
|
export { default as LoginForm } from './components/login/LoginForm.svelte';
|
|
21
25
|
export { default as LoginDialog } from './components/login/LoginDialog.svelte';
|
|
26
|
+
export { default as SignupPermissions } from './components/login/SignupPermissions.svelte';
|
|
22
27
|
export { default as ActorSearchField } from './components/login/ActorSearchField.svelte';
|
|
23
28
|
export { default as RecentAccounts } from './components/login/RecentAccounts.svelte';
|
|
24
29
|
export { createActorSearch } from './components/login/search.js';
|
|
25
|
-
export { normalizeHandle, isValidHandle } from './components/login/handle.js';
|
|
30
|
+
export { normalizeHandle, isValidHandle, normalizeIdentifier, isValidIdentifier } from './components/login/handle.js';
|
|
26
31
|
export { resolveLoginOptions, isPermissionSelected } from './components/login/permissions.js';
|
|
27
32
|
export { loadRecentAccounts, writeRecentAccounts, saveRecentAccount, removeRecentAccount, RECENT_ACCOUNTS_STORAGE_KEY } from './components/login/recent-accounts.js';
|
|
28
33
|
export { default as ToggleGroup } from './components/ToggleGroup.svelte';
|
package/dist/legal.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
/** Framework-free authoring helpers for scripts; UI components use the main entry. */
|
|
2
|
+
export { createLegalDrafts, getLegalDraftIssues, getLegalDocumentIssues } from './components/legal/drafts.js';
|
|
3
|
+
export type { LegalDocumentData, LegalDocumentKind, LegalDraftConfig, LegalSection, LegalFooterLinks } from './components/legal/types.js';
|
package/dist/legal.js
ADDED
package/dist/theme.css
CHANGED
package/dist/theme.js
CHANGED
|
@@ -19,5 +19,12 @@ export function observeTheme(element, update) {
|
|
|
19
19
|
});
|
|
20
20
|
parent = parent.parentElement;
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
// readTheme snapshots computed semantic colors for portals. Those colors
|
|
23
|
+
// change with the system scheme even when no ancestor attribute changes.
|
|
24
|
+
const scheme = window.matchMedia('(prefers-color-scheme: dark)');
|
|
25
|
+
scheme.addEventListener('change', update);
|
|
26
|
+
return () => {
|
|
27
|
+
observer.disconnect();
|
|
28
|
+
scheme.removeEventListener('change', update);
|
|
29
|
+
};
|
|
23
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atmo-dev/kit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "The shared UI foundation for Atmo apps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"svelte": "./dist/index.js",
|
|
28
28
|
"default": "./dist/index.js"
|
|
29
29
|
},
|
|
30
|
+
"./legal": {
|
|
31
|
+
"types": "./dist/legal.d.ts",
|
|
32
|
+
"default": "./dist/legal.js"
|
|
33
|
+
},
|
|
30
34
|
"./theme.css": "./dist/theme.css"
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": {
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
-
<path d="M52.0483 0.93099C53.1501 0.921438 53.8535 1.06037 54.7446 1.75893C55.4873 2.51347 56.0296 3.42362 56.0224 4.50009C56.0668 18.0153 55.9707 31.5308 56.0122 45.0458C56.0104 47.5675 54.4686 47.3201 54.4661 47.3197C54.4917 50.3233 54.4674 53.3311 54.5447 56.3337C54.5377 57.548 54.2466 58.5049 52.4863 58.5064L25.7792 58.5727C25.5946 58.5638 25.5946 58.5638 25.5037 58.6744C25.4938 58.9353 25.556 61.6779 25.5561 61.6808C25.5465 62.144 25.5531 62.9893 24.5112 63C22.8066 63.0173 21.1565 60.7507 20.2706 60.7471C19.5918 60.7443 17.8008 63.0001 15.9265 62.9913C15.4985 62.9893 15.1922 62.6836 15.0376 62.3692C14.9527 62.0605 14.9636 61.7672 14.9759 61.4491C14.9864 60.4343 15.009 59.4184 15.0376 58.404L14.8658 58.4065C13.433 58.4097 7.98682 58.5339 7.98736 52.2753C7.9386 37.3459 7.96021 22.4163 7.93185 7.48695C7.91841 0.890681 13.5317 0.96174 14.244 0.963904L14.2441 0.963816C26.8455 0.919962 39.4471 1.04933 52.0483 0.93099ZM17.2031 59.9361C18.0694 59.417 18.9226 58.7165 19.8728 58.3675C20.4465 58.2263 20.8562 58.4527 21.3546 58.7478C21.6127 58.9016 23.0463 59.846 23.3384 60.0262V52.8169H17.2031V59.9361ZM15.4946 47.828C14.4598 47.8221 13.5842 47.8236 12.6684 48.3886C11.3934 49.2216 10.5772 50.4986 10.5146 52.0667C10.5581 55.439 12.7708 56.2239 14.2018 56.2472C14.4503 56.2452 14.6988 56.2438 14.9473 56.2413C14.9699 54.7203 14.992 53.1994 15.0191 51.6785C15.0259 51.2477 15.0446 51.1028 15.3475 50.7816C15.661 50.5291 15.9258 50.5093 16.3203 50.5029C18.8985 50.5305 21.4781 50.5048 24.0565 50.5057C24.4337 50.5016 24.7923 50.479 25.1362 50.6507C25.3693 50.8243 25.4919 50.9727 25.5717 51.2522L25.5841 55.4208C25.5834 55.6761 25.5701 55.8331 25.6842 56.061C25.8799 56.1848 26.0249 56.2391 26.256 56.2636C34.8173 56.2573 43.3785 56.2521 51.9398 56.2413V56.061C50.6253 53.3866 50.8985 50.5477 51.9398 47.8604C47.2275 47.8544 16.0841 47.8265 15.4946 47.828ZM13.3397 3.28933C12.4325 3.36836 10.0574 3.73972 10.1655 7.98162C10.1696 8.15242 10.2369 41.5329 10.2557 47.3197V47.4099C11.1579 46.5988 11.6431 46.0768 13.2331 45.7878C13.2331 30.2549 13.3397 17.9797 13.3397 3.28933ZM50.3365 3.15107C48.4414 3.15161 17.5151 3.18598 15.5332 3.25861L15.5516 45.7417C21.1475 45.811 50.6824 45.7303 51.4888 45.6977C53.3652 45.6221 53.7518 45.0956 53.7449 43.0277C53.7162 34.341 53.665 9.58593 53.6781 6.36745C53.6894 3.60523 53.4421 3.15009 50.3365 3.15107ZM29.9248 4.69475C30.8644 4.69476 31.6539 5.51957 31.5369 6.46232C32.4026 7.41817 33.0369 8.63188 33.5401 9.78791C33.6893 10.1309 33.8146 10.4575 33.99 10.7738C34.5922 10.398 37.7986 10.5099 38.1152 10.8594L38.1807 10.4479C38.4829 9.04123 39.2361 7.92084 40.3014 7.08591C40.2725 6.14568 41.1018 5.41571 42.015 5.41568C42.9618 5.41569 43.7293 6.14192 43.7293 7.03778C43.7293 8.19237 42.4891 8.91219 41.4507 8.56984C40.8876 8.99859 40.7576 9.09367 40.1033 10.6914C40.0479 10.8267 39.9062 11.0117 40.043 11.094C45.13 14.151 45.7123 16.9707 45.7682 20.076L45.9645 20.0541C49.3503 19.1903 51.9897 19.8251 52.4776 22.7094C52.5577 23.7237 52.3694 24.602 51.9236 25.506C50.8378 27.5413 46.4796 30.5203 44.1957 29.951L44.2067 30.3422C43.9504 46.0599 29.3295 40.2729 29.3091 40.2648C29.3009 40.27 27.3183 41.5417 26.9916 41.4083C26.6049 41.336 27.4322 39.6021 27.5268 39.2773L27.3114 39.0357C24.4702 37.2377 23.9692 34.9197 23.8575 32.8038L23.6004 32.9106C17.0555 35.3729 18.0353 26.8788 21.5512 26.001L21.3218 25.7609C11.2829 15.4735 20.5036 10.6607 27.422 16.7844C27.4229 16.7794 28.1502 12.7938 32.0348 11.2983C31.5022 10.0545 30.8877 8.99646 30.2278 7.82128C29.2712 7.99657 28.3007 7.27154 28.3007 6.27179C28.3007 5.40083 29.0279 4.69477 29.9248 4.69475ZM26.8979 30.0607C23.5884 38.2578 40.4533 43.2877 41.8894 33.6816C39.6793 34.3829 31.953 36.0684 27.0351 30.2549L26.8979 30.0607ZM24.7736 27.2341C23.3243 26.9989 22.1689 27.7022 22.0472 27.765C20.2551 28.6908 19.4806 33.3183 23.2802 31.4185C23.8598 31.217 23.9631 30.0229 24.2466 29.456C24.3874 29.1715 25.036 27.8224 25.4301 27.3177C25.1065 27.2496 25.0382 27.277 24.7736 27.2341ZM30.2526 25.3427C28.9557 25.9328 28.2918 27.8003 28.2906 27.8038C28.3011 27.8227 31.6274 33.7806 41.8894 30.7191C42.0919 29.6972 41.7578 27.5658 41.1546 27.118C35.5948 28.8958 31.9491 27.1244 30.2526 25.3427ZM50.8069 22.1813C48.742 19.9078 45.4885 22.4437 45.2873 22.7107C44.95 24.1274 43.9692 25.058 43.0228 25.8091C42.8865 25.905 42.7532 26.0057 42.6231 26.1107C42.6232 26.3302 43.7375 28.4638 43.929 28.7153C43.9383 28.715 46.1981 28.6274 48.7523 26.4433C48.7653 26.4318 52.0258 23.5235 50.8069 22.1813ZM41.2457 14.5195C37.3692 10.7942 29.9837 12.9105 29.3624 17.939C29.1447 19.7002 29.6713 21.3039 30.672 22.7094L30.8816 22.9289C31.7128 23.8894 32.8151 24.6313 33.962 25.1032C36.2257 25.8783 38.5669 25.7495 40.7078 24.6468C42.1238 23.8195 43.0442 22.8138 43.579 20.8295C44.1959 16.7171 41.61 14.833 41.2457 14.5195ZM23.8765 16.9841C18.1211 13.6901 16.1166 20.3808 25.4116 24.9413C25.4169 24.9451 26.4861 25.7192 27.5192 25.2656C28.0798 24.9605 28.6565 24.5529 28.8896 23.9164C28.0922 22.8984 27.4276 21.8552 27.232 20.5424C27.1994 20.2719 27.1609 20.0146 27.1075 19.7469C26.4768 18.4447 24.9091 17.5788 23.8765 16.9841ZM36.6935 22.4214C36.1914 21.7363 37.1875 21.1485 37.4735 21.7041C37.7164 22.176 38.2534 22.4599 38.7605 22.4398C39.307 22.4181 39.7609 22.1519 40.0795 21.7229C40.3222 21.3958 41.1377 21.8847 40.8264 22.341C39.8869 23.718 37.5936 23.6497 36.6935 22.4214ZM36.4843 17.9797C37.0215 17.9797 37.457 18.5762 37.457 19.312C37.457 20.0478 37.0215 20.6442 36.4843 20.6443C35.9471 20.6443 35.5115 20.0478 35.5115 19.312C35.5115 18.5762 35.9471 17.9797 36.4843 17.9797ZM40.77 17.9797C41.2324 17.9797 41.6074 18.5359 41.6074 19.2219C41.6074 19.9079 41.2324 20.464 40.77 20.464C40.3075 20.464 39.9326 19.9079 39.9326 19.2219C39.9326 18.5359 40.3075 17.9797 40.77 17.9797Z" fill="#210E03"/>
|
|
3
|
-
</svg>
|