@atmo-dev/kit 0.9.0 → 0.10.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 +27 -429
- package/dist/components/AccountMenu.svelte +7 -7
- package/dist/components/Avatar.svelte +1 -1
- package/dist/components/Badge.svelte +3 -3
- package/dist/components/Button.svelte +4 -3
- package/dist/components/Checkbox.svelte +31 -1
- package/dist/{events → components}/DatePicker.svelte +3 -3
- package/dist/components/ExternalEmbed.svelte +9 -6
- package/dist/components/GrainAlbumPicker.svelte +18 -9
- package/dist/components/GrainAlbumView.svelte +15 -6
- package/dist/components/Heading.svelte +1 -1
- package/dist/components/IconRail.svelte +11 -9
- package/dist/components/InlineEditor.svelte +3 -3
- package/dist/components/InsetImage.svelte +1 -1
- package/dist/components/Label.svelte +1 -1
- package/dist/components/LinkCard.svelte +6 -4
- package/dist/components/Modal.svelte +3 -3
- package/dist/components/NestedComments.svelte +9 -6
- package/dist/components/PhotoGrid.svelte +1 -1
- package/dist/components/Popover.svelte +1 -1
- package/dist/components/PostComposer.svelte +2 -2
- package/dist/components/PostContent.svelte +11 -9
- package/dist/components/PostVideo.svelte +1 -1
- package/dist/components/RichText.svelte +19 -7
- package/dist/components/Select.svelte +5 -2
- package/dist/components/Select.svelte.d.ts +2 -0
- package/dist/components/SocialClientPicker.svelte +1 -1
- package/dist/components/StreamplacePicker.svelte +1 -1
- package/dist/components/Subheading.svelte +4 -2
- package/dist/components/Surface.svelte +3 -2
- package/dist/components/Tabs.svelte +2 -2
- package/dist/{events → components}/TimePicker.svelte +10 -7
- package/dist/{events → components}/TimezonePicker.svelte +15 -7
- package/dist/components/ToggleGroup.svelte +1 -1
- package/dist/components/ToggleGroupItem.svelte +1 -1
- package/dist/components/UserProfile.svelte +16 -9
- package/dist/components/WorkspaceSidebar.svelte +1 -1
- package/dist/components/legal/LegalDocument.svelte +9 -3
- package/dist/components/legal/LegalFooter.svelte +2 -2
- package/dist/components/login/ActorSearchField.svelte +10 -6
- package/dist/components/login/AtmosphereApps.svelte +1 -1
- package/dist/components/login/LoginForm.svelte +15 -9
- package/dist/components/login/PermissionFields.svelte +9 -2
- package/dist/components/login/RecentAccounts.svelte +5 -3
- package/dist/events/EventAttendees.svelte +9 -3
- package/dist/events/EventCard.svelte +7 -5
- package/dist/events/EventComments.svelte +3 -3
- package/dist/events/EventEditor.svelte +20 -9
- package/dist/events/EventRsvp.svelte +10 -6
- package/dist/events/EventView.svelte +21 -7
- package/dist/events/StarterPackButton.svelte +3 -3
- package/dist/events/ThemePicker.svelte +7 -7
- package/dist/events/ThumbnailPresets.svelte +1 -1
- package/dist/events/VodPlayer.svelte +1 -1
- package/dist/events/editor/LinksSection.svelte +7 -3
- package/dist/events/editor/LocationSection.svelte +1 -1
- package/dist/events/editor/RecurringModal.svelte +6 -4
- package/dist/events/editor/ThemeSection.svelte +3 -1
- package/dist/events/event-view/EventDateBlock.svelte +3 -3
- package/dist/events/event-view/EventHostedBy.svelte +6 -2
- package/dist/events/event-view/EventLinksList.svelte +2 -2
- package/dist/events/event-view/EventLocationBlock.svelte +9 -5
- package/dist/events/event-view/EventLocationMap.svelte +4 -2
- package/dist/events/schedule/ConferenceTimetable.svelte +7 -7
- package/dist/events/schedule/DaySchedule.svelte +2 -2
- package/dist/events/schedule/ScheduleEventCell.svelte +2 -2
- package/dist/events/themes/Matrix.svelte +12 -6
- package/dist/events.d.ts +5 -5
- package/dist/events.js +6 -5
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/social/mention-suggestions.js +4 -4
- package/dist/styles.d.ts +2 -2
- package/dist/styles.js +2 -2
- package/dist/theme.css +23 -44
- package/dist/theme.js +4 -12
- package/package.json +1 -1
- /package/dist/{events → components}/DatePicker.svelte.d.ts +0 -0
- /package/dist/{events → components}/DateTimePicker.svelte +0 -0
- /package/dist/{events → components}/DateTimePicker.svelte.d.ts +0 -0
- /package/dist/{events → components}/ImageDropper.svelte +0 -0
- /package/dist/{events → components}/ImageDropper.svelte.d.ts +0 -0
- /package/dist/{events → components}/TimePicker.svelte.d.ts +0 -0
- /package/dist/{events → components}/TimezonePicker.svelte.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,449 +1,47 @@
|
|
|
1
1
|
# Atmo Kit
|
|
2
2
|
|
|
3
|
-
Svelte 5 and Tailwind CSS 4 components for
|
|
3
|
+
Svelte 5 and Tailwind CSS 4 components for Atmosphere apps.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- Develop: `pnpm install && pnpm dev`. Components at `/components`, app mockups at `/preview`.
|
|
8
|
-
- Validate: `pnpm lint && pnpm check && pnpm test:ci && pnpm build`.
|
|
9
|
-
- Releases: `pnpm changeset`, then merge the generated release PR. [Details](.changeset/README.md).
|
|
10
|
-
- License: [MIT](LICENSE).
|
|
5
|
+
Buttons, forms, navigation, login UI, profiles, posts, photos, and event UI.
|
|
6
|
+
Apps own authentication, data fetching, routes, and persistence.
|
|
11
7
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
`IconRail` items accept a `variant` from `Button` (`primary`, `secondary`, `ghost`,
|
|
15
|
-
`link`); omitted variants remain `ghost`. Use `primary: true, variant: 'primary'`
|
|
16
|
-
for a separated, filled creation action in the rail, mobile bar or drawer. The
|
|
17
|
-
existing `primary` flag alone retains its subtle accent/spacing behavior.
|
|
18
|
-
|
|
19
|
-
### Sidebar account menu
|
|
20
|
-
|
|
21
|
-
Use `AccountMenu` in a signed-in sidebar footer. Its avatar button opens just
|
|
22
|
-
**Go to profile** and **Sign out**; there is no account switching. `expanded`
|
|
23
|
-
adds the name and optional handle for wider sidebars and mobile drawers.
|
|
24
|
-
|
|
25
|
-
```svelte
|
|
26
|
-
<IconRail label="Navigation" {items}>
|
|
27
|
-
{#snippet footer({ expanded, close })}
|
|
28
|
-
{#if viewer}
|
|
29
|
-
<AccountMenu
|
|
30
|
-
name={viewer.displayName || viewer.handle}
|
|
31
|
-
handle={viewer.handle}
|
|
32
|
-
avatar={viewer.avatar}
|
|
33
|
-
profileHref={`/p/${encodeURIComponent(viewer.did)}`}
|
|
34
|
-
{expanded}
|
|
35
|
-
onprofile={close}
|
|
36
|
-
onsignout={() => {
|
|
37
|
-
close();
|
|
38
|
-
signOut();
|
|
39
|
-
}}
|
|
40
|
-
/>
|
|
41
|
-
{/if}
|
|
42
|
-
{/snippet}
|
|
43
|
-
</IconRail>
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Both components are exported from `@atmo-dev/kit`. Use `profileHref` for a native
|
|
47
|
-
link, or omit it and supply `onprofile` for app-owned navigation. Apps own the
|
|
48
|
-
profile route, session, sign-out request, and signed-out UI. The menu handles
|
|
49
|
-
keyboard navigation, dismissal, focus restoration and portaled theme inheritance.
|
|
50
|
-
|
|
51
|
-
## Safe link destinations
|
|
52
|
-
|
|
53
|
-
Use the dependency-free `@atmo-dev/kit/urls` entry in server loaders and components:
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
import { safeHttpUrl, safeLinkHref } from '@atmo-dev/kit/urls';
|
|
57
|
-
|
|
58
|
-
const website = safeHttpUrl(record.url);
|
|
59
|
-
const navigation = safeLinkHref(card.href);
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
- `safeHttpUrl(unknown)` accepts absolute HTTP(S) URLs without usernames/passwords
|
|
63
|
-
and returns their normalized URL. Use it for untrusted external record URLs.
|
|
64
|
-
- `safeLinkHref(unknown)` additionally allows root-relative app paths, fragments,
|
|
65
|
-
`mailto:` and `tel:` actions. Protocol-relative paths and backslash/control-character
|
|
66
|
-
local/contact links are rejected. It does not resolve `./` or `../` relative paths.
|
|
67
|
-
- Invalid input returns `undefined`: pass the result to `href`, or omit the link.
|
|
68
|
-
Never fall back to the original untrusted value. `target="_blank"` is not URL validation.
|
|
69
|
-
|
|
70
|
-
The helpers perform no IO and import no Svelte, DOM or Markdown parser. They are also
|
|
71
|
-
exported from the main Kit entry. Existing `safeWebUrl` exports from richtext remain
|
|
72
|
-
aliases of `safeHttpUrl`. Rich-text mention callbacks and legal links retain their
|
|
73
|
-
stricter destination policies. Generic UI primitives such as Button still accept
|
|
74
|
-
app-owned routes; validate untrusted values before passing them in.
|
|
75
|
-
|
|
76
|
-
These are **navigation** policies, not HTML sanitizers or SSRF protection. Image,
|
|
77
|
-
iframe and media sources may require tighter policies. Server-side fetching needs
|
|
78
|
-
separate host/IP, DNS, redirect and response-content checks.
|
|
79
|
-
|
|
80
|
-
## AT record metadata (AT Tags)
|
|
81
|
-
|
|
82
|
-
Use `AtTags` to let other clients discover which AT Protocol records and identities
|
|
83
|
-
an HTML page represents. It renders server-side `<meta name="at:…" content="at://…">`
|
|
84
|
-
tags in the document head and updates/removes them when its props change. It needs
|
|
85
|
-
no theme, styles, network requests, or app state.
|
|
86
|
-
|
|
87
|
-
The [AT Tags proposal](https://tangled.org/chrisshank.com/at-tags) is a draft. Its four
|
|
88
|
-
standard relationships are:
|
|
89
|
-
|
|
90
|
-
| Property | Meaning | Content |
|
|
91
|
-
| ----------- | ------------------------------------------------------------------------ | --------------- |
|
|
92
|
-
| `canonical` | The record this page represents; deleting it would remove the page. | A record AT URI |
|
|
93
|
-
| `alternate` | Auxiliary records shown or referenced; the page survives their deletion. | A record AT URI |
|
|
94
|
-
| `author` | Account(s) who authored the page's content. | `at://<DID>` |
|
|
95
|
-
| `me` | Account(s) this site or section belongs to. | `at://<DID>` |
|
|
96
|
-
|
|
97
|
-
Each property accepts an array; multiple values produce separate tags, with exact
|
|
98
|
-
duplicates removed within each property. Use meta **names**, not Open Graph
|
|
99
|
-
`property` attributes or `<link href="at://…">`. These tags supplement—not
|
|
100
|
-
replace—your title, description, HTTPS canonical URL and Open Graph metadata.
|
|
101
|
-
|
|
102
|
-
For a page backed by one record authored by its repository owner, use the
|
|
103
|
-
framework-free `recordAtTags` helper in your server loader:
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import { recordAtTags } from '@atmo-dev/kit/at-tags';
|
|
107
|
-
|
|
108
|
-
// After app-owned loading, visibility/moderation checks and handle resolution:
|
|
109
|
-
return {
|
|
110
|
-
atTags: recordAtTags(record, relatedRecords)
|
|
111
|
-
};
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
`record` and each optional `relatedRecords` entry only need `{ uri: string }`.
|
|
115
|
-
The helper sets `canonical`, derives `author` from the record's repository DID,
|
|
116
|
-
and lists related records as `alternate`, excluding the canonical URI. It throws
|
|
117
|
-
`SyntaxError` for non-record or handle-based URIs: pass resolved DID-based record
|
|
118
|
-
URIs, not a URL path or viewer parameter. It does not resolve or fetch anything.
|
|
119
|
-
|
|
120
|
-
Render the metadata once in a SvelteKit root layout (covering embeds too):
|
|
121
|
-
|
|
122
|
-
```svelte
|
|
123
|
-
<script lang="ts">
|
|
124
|
-
import { page } from '$app/state';
|
|
125
|
-
import { AtTags } from '@atmo-dev/kit';
|
|
126
|
-
|
|
127
|
-
let { children } = $props();
|
|
128
|
-
</script>
|
|
129
|
-
|
|
130
|
-
<AtTags tags={page.error ? undefined : page.data.atTags} />
|
|
131
|
-
{@render children()}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Add `atTags?: import('@atmo-dev/kit/at-tags').AtTagMetadata` to `App.PageData`.
|
|
135
|
-
In other Svelte apps, pass the current page's metadata directly; set `tags` to
|
|
136
|
-
`undefined` on pages with no relationships. Do not mount competing tag owners.
|
|
137
|
-
|
|
138
|
-
You can supply `AtTagMetadata` directly for multiple canonical records, different
|
|
139
|
-
or additional authors, or an account section:
|
|
140
|
-
|
|
141
|
-
```ts
|
|
142
|
-
import type { AtTagMetadata } from '@atmo-dev/kit/at-tags';
|
|
143
|
-
|
|
144
|
-
const atTags: AtTagMetadata = {
|
|
145
|
-
me: [`at://${profile.did}`],
|
|
146
|
-
alternate: profile.uri ? [profile.uri] : []
|
|
147
|
-
};
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
An account's events page might survive deletion of its optional Bluesky profile,
|
|
151
|
-
so that profile record is an alternate, not necessarily canonical. Likewise,
|
|
152
|
-
embedded comments are alternate records, not the page's authors. `me` is not the
|
|
153
|
-
signed-in viewer or every record author; do not assign it site-wide unless the
|
|
154
|
-
site actually belongs to that account. Ordinary discovery and creation pages
|
|
155
|
-
usually have no canonical record.
|
|
156
|
-
|
|
157
|
-
Apps own these semantic choices, record loading, identity resolution, visibility,
|
|
158
|
-
and error-page suppression. The component escapes attributes but does not validate
|
|
159
|
-
raw metadata or verify record existence. Use record URIs with collection and rkey,
|
|
160
|
-
and bare `at://<DID>` identity URIs without a trailing slash. Only the four standard
|
|
161
|
-
properties are supported; parsing/scraping pages and draft custom namespaces are
|
|
162
|
-
outside this API.
|
|
163
|
-
|
|
164
|
-
## Events
|
|
165
|
-
|
|
166
|
-
The event UI formerly in `@atmo-dev/events-ui` lives in this package:
|
|
167
|
-
|
|
168
|
-
```svelte
|
|
169
|
-
<script lang="ts">
|
|
170
|
-
import { EventEditor, EventView, EventCard } from '@atmo-dev/kit/events';
|
|
171
|
-
import type { EditorAdapter, EditorViewer } from '@atmo-dev/kit/events-data';
|
|
172
|
-
|
|
173
|
-
let { adapter, viewer, event, data, pageUrl } = $props();
|
|
174
|
-
</script>
|
|
175
|
-
|
|
176
|
-
<EventCard {event} />
|
|
177
|
-
<EventEditor {adapter} {viewer} eventData={event} actorDid={event.did} rkey={event.rkey} />
|
|
178
|
-
<EventView {adapter} {viewer} {data} {pageUrl} />
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
- `@atmo-dev/kit/events`: event cards, views, editor, RSVP/attendee/comment UI, starter-pack actions, conference timetable, VOD player/transcript and date/time/image controls. Event backgrounds and editor sections stay internal.
|
|
182
|
-
- `@atmo-dev/kit/events-data`: structural event types, adapter contracts, conference/date helpers, URL helpers, theme values, linked-record types and thumbnail selection. This entry is safe in plain Node and server routes: no Svelte, CSS, maps or players are loaded. Browser helpers such as `compressImage` only access browser APIs when called.
|
|
183
|
-
- `@atmo-dev/kit/events/editor`: `buildEventRecord` and `buildThumbnailMedia` for hosts that need explicit record construction. This separate entry includes browser-oriented thumbnail code; it is not part of the pure data API.
|
|
184
|
-
|
|
185
|
-
Event descriptions are Markdown with AT Protocol facets. Parse them on the server and pass the result as `data.description`, so the Markdown parser stays out of the client bundle:
|
|
186
|
-
|
|
187
|
-
```ts
|
|
188
|
-
import { parseRichText } from '@atmo-dev/kit/richtext';
|
|
189
|
-
|
|
190
|
-
const description = event.description
|
|
191
|
-
? parseRichText(event.description, { facets: event.facets, markdown: true, detectLinks: true })
|
|
192
|
-
: null;
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
Without `data.description`, `EventView` shows the description as plain text with facet links. Description headings render one level below the event title, and the Bluesky share card uses `richTextToPlainText(description)`. Conference schedules show talk descriptions as short previews: pass `conferenceTalkDescriptions`, plain text keyed by talk URI, so Markdown syntax does not appear there. See [Rich text](#rich-text) for what is supported.
|
|
196
|
-
|
|
197
|
-
Event components read Bluesky's public AppView and CDN by default. Apps using other services call `configureAtproto({ appview, cdn })` from `events-data` once, before rendering, on both the server and the client (for example at the top of the root layout).
|
|
198
|
-
|
|
199
|
-
Event cards and covers prefer native `media` images, then an HTTPS `poster.url`, then a URI named `Event Image` (laugh.town compatibility). `getEventExternalImage(event)` from `events-data` returns `{ url, alt, aspectRatio? }` or `null`, validating URLs and optional poster dimensions. It performs no IO; apps must apply their own allowlist/redirect policy before fetching an external image server-side.
|
|
200
|
-
|
|
201
|
-
Apps implement `EditorAdapter` for authentication prompts, repository IO, uploads, save/delete navigation, and optional linked-record transport/indexing notifications. Keep sessions, Cloudflare bindings, app routes, server endpoints, caching and moderation in the app. The editor/view never imports a consumer's app state. Existing `EventView` page-data shape and RSVP behavior are preserved in this migration.
|
|
202
|
-
|
|
203
|
-
Full event UI requires optional peers (ordinary Kit consumers do not):
|
|
8
|
+
## Installation
|
|
204
9
|
|
|
205
10
|
```sh
|
|
206
|
-
pnpm add
|
|
11
|
+
pnpm add @atmo-dev/kit
|
|
207
12
|
```
|
|
208
13
|
|
|
209
|
-
Import
|
|
210
|
-
|
|
211
|
-
The initial move preserves legacy defaults: `/p/:actor/e/:rkey` event and `/p/:actor` profile links, `/api/geocoding`, Bluesky's public comments API, the `?created` share flow and RSVP's `createdWith` identifier. Hosts currently need compatible routes/endpoints; `shareUrlOverride` changes outbound share links. These defaults are not a new universal routing contract. Further host-policy extraction can happen independently of the package move.
|
|
14
|
+
Import the theme after Tailwind in your global CSS:
|
|
212
15
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`PhotoGrid` and `PhotoLightbox` work with provider-neutral `Photo[]` data (`src`, optional `thumbnail`, `alt`, dimensions and `contentWarning`). The grid preserves natural image proportions in responsive masonry columns, with a subtle 2% hover zoom disabled for reduced motion. It gates warned images before mounting them and opens a borderless fullscreen viewer. The viewer has a dimmed backdrop, previous/next arrows, close and a small photo-position counter; native horizontal scrolling with CSS scroll snap makes photos follow your finger or trackpad, with inertia rather than switching only on release. Mouse/pen drags also move the track directly. Swipes loop like the arrows; vertical pans do not navigate and controls are separate from the drag surface. Native pinch zoom, arrow keys, Escape, focus trapping and restoration are preserved; release animations respect reduced motion. `PhotoLightbox` also works independently with `bind:open` and `bind:index`; callers using it directly must first filter or reveal any warned photos. Apps own URL validation and moderation policy.
|
|
218
|
-
|
|
219
|
-
`GrainAlbumPicker`, `GrainAlbumView` and `StreamplacePicker` take explicit callbacks. They own presentation, pending/error states and stale-read protection—not sessions, event records, persistence, routes or caching. Picker selection callbacks can return a user-facing error string to keep the dialog open. See the offline examples at `/components`.
|
|
220
|
-
|
|
221
|
-
```svelte
|
|
222
|
-
<GrainAlbumPicker
|
|
223
|
-
bind:open
|
|
224
|
-
actor={viewer.did}
|
|
225
|
-
listAlbums={photos.listAlbums}
|
|
226
|
-
loadAlbum={photos.getGallery}
|
|
227
|
-
onselect={(album) => attachAlbum(album)}
|
|
228
|
-
/>
|
|
229
|
-
<GrainAlbumView reference={albumUri} loadAlbum={photos.getGallery} />
|
|
230
|
-
<StreamplacePicker
|
|
231
|
-
bind:open={streamOpen}
|
|
232
|
-
actor={viewer.handle}
|
|
233
|
-
{resolveHandle}
|
|
234
|
-
onselect={(stream) => attachStream(stream.url)}
|
|
235
|
-
/>
|
|
16
|
+
```css
|
|
17
|
+
@import 'tailwindcss';
|
|
18
|
+
@import '@atmo-dev/kit/theme.css';
|
|
236
19
|
```
|
|
237
20
|
|
|
238
|
-
`
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`parseGrainLink` is exported from `@atmo-dev/kit/grain`, `parseStreamplaceLink` from `@atmo-dev/kit/streamplace`, and `Photo` from `@atmo-dev/kit/media`. The main entry point exports these too. Grain URLs normalize to DID-based links after loading; handles and percent-encoded DID identifiers are accepted. These helpers validate syntax only, not account existence.
|
|
243
|
-
|
|
244
|
-
## Read-only profiles
|
|
21
|
+
Apply `class="atmo zinc pink light"` (or `dark`) to `<html>`. Omit `light`/`dark`
|
|
22
|
+
to follow browser preferences. Kit supplies `dark:`; no app-specific variant is
|
|
23
|
+
needed. See [colors and dark mode](docs/colors.md).
|
|
245
24
|
|
|
246
|
-
|
|
25
|
+
Import components from `@atmo-dev/kit`; event components use `@atmo-dev/kit/events`.
|
|
247
26
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
import { UserProfile, blueskyProfileToProfileData } from '@atmo-dev/kit';
|
|
27
|
+
See the [component overview](docs/components.md) and [guides](docs/README.md)
|
|
28
|
+
for available components, examples, and integration details.
|
|
251
29
|
|
|
252
|
-
|
|
253
|
-
let { profileView } = $props();
|
|
254
|
-
let profile = $derived(blueskyProfileToProfileData(profileView));
|
|
255
|
-
</script>
|
|
30
|
+
## Development
|
|
256
31
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
`ProfileData` can also be supplied directly. Bios remain escaped plain text with detected HTTP(S) links, mentions and hashtags; newlines are preserved. There is no HTML/Markdown interpretation, DOM sanitizer, client-only rendering, or mention-resolution request. Link destinations and images reject executable schemes and credential-bearing URLs. External links use `noopener noreferrer`; user links also use `nofollow ugc`.
|
|
261
|
-
|
|
262
|
-
`blueskyProfileToProfileData` accepts an `app.bsky.actor.getProfile` view and validates optional fields. Its `links` must be hydrated `app.bsky.actor.link` views (`url`, optional `title` and `icon` image URL), not the profile record's ordered strongRefs. It omits unresolved refs and labeled links conservatively, deduplicates destinations, and displays at most ten. The legacy `website` field is deliberately ignored. Apps own hydration, account moderation and any more specific link-moderation policy. Supplied icons render as small decorative images; unsafe or failed icons fall back to text-only links. No link metadata or destination is fetched by the component.
|
|
263
|
-
|
|
264
|
-
## Bluesky comments and embeds
|
|
265
|
-
|
|
266
|
-
`NestedComments` renders `PostData[]` with recursive replies and compact reply, repost, and like icon/count links. Counts default to zero; each link has a full accessible label and opens the post in the viewer's preferred social client. These are outbound links, not authenticated engagement buttons.
|
|
267
|
-
|
|
268
|
-
Use `blueskyPostToPostData(postView)` to convert public AppView posts, including `repostCount`. `PostContent` supports images, galleries, external link cards, HLS video, quoted posts (including their embeds), record-with-media combinations, feeds, lists, starter packs, and moderation-service cards. Missing, blocked, and detached quotes have readable placeholders; unknown embed variants show an unavailable fallback. Quote nesting is capped at three levels. Non-post record cards use Bluesky routes, since other clients may not implement them. Apps still own fetching and moderation.
|
|
269
|
-
|
|
270
|
-
### Rich text
|
|
271
|
-
|
|
272
|
-
`RichText` renders text shared by posts, event descriptions and other user content. Plain text can be passed directly:
|
|
273
|
-
|
|
274
|
-
```svelte
|
|
275
|
-
<RichText
|
|
276
|
-
text={post.text}
|
|
277
|
-
facets={post.facets}
|
|
278
|
-
{socialClient}
|
|
279
|
-
mentionUrl={(actor) => `/p/${encodeURIComponent(actor)}`}
|
|
280
|
-
/>
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
For Markdown, parse with `@atmo-dev/kit/richtext` (no Svelte or DOM; works in Workers) and pass `content`:
|
|
284
|
-
|
|
285
|
-
```ts
|
|
286
|
-
// +page.server.ts
|
|
287
|
-
import { parseRichText } from '@atmo-dev/kit/richtext';
|
|
288
|
-
|
|
289
|
-
const content = parseRichText(text, { facets, markdown: true, detectLinks: true });
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
```svelte
|
|
293
|
-
<RichText {content} {socialClient} headingOffset={1} />
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
Each feature can be turned on separately:
|
|
297
|
-
|
|
298
|
-
- `markdown`: CommonMark plus `~~strikethrough~~`. Single newlines are line breaks. Raw HTML is shown as written. Images are never loaded; they become links labeled by their alt text. Tables and other extensions stay literal text. Text over 10,000 characters, with more than 1,000 `*`, `_`, `~` or `[` characters, or with more than 8 nested quote or list markers on a line, is rendered as plain text, because the parser slows down quadratically on some inputs. Without `markdown`, text is plain and whitespace is preserved.
|
|
299
|
-
- `facets`: record facets are validated at runtime. Malformed, overlapping or mid-character ranges are ignored without dropping text. In Markdown, facets apply to formatted text such as `**@alice**`; facets in code, inside Markdown links, across Markdown syntax or on text changed by escapes or entities are ignored.
|
|
300
|
-
- `detectLinks`: detects URLs, mentions and hashtags when there are no facets, outside code and existing links. Detection uses the mention, hashtag and URL rules of `@atcute/bluesky-richtext-parser` in one linear pass. Its `tokenize` also parses emphasis and backtracks badly on runs of `*` (a hundred take minutes), so do not run it on untrusted text; `detectTokens` from `@atmo-dev/kit/richtext` returns compatible tokens.
|
|
301
|
-
|
|
302
|
-
Links accept only HTTP(S) URLs without credentials. `socialClient` selects mention and hashtag destinations; `mentionUrl` overrides mention routing with an HTTP(S) URL or root-relative path. Local links stay in the same tab; external links use `noopener noreferrer nofollow ugc`. Post text keeps Bluesky destinations by default.
|
|
303
|
-
|
|
304
|
-
The parsed `RichTextDocument` is plain JSON with a `version`, independent of the viewer, so it can be sent in page data and cached publicly. `RichText` still treats it as untrusted: it renders only known elements, checks every link again and shows unknown nodes as text. Prefer parsing per request (a few milliseconds) over storing documents. `richTextToPlainText(content)` returns text without Markdown syntax for meta descriptions and previews.
|
|
305
|
-
|
|
306
|
-
To parse in the browser, for example in an editor preview, load the parser on demand with `await import('@atmo-dev/kit/richtext')` so it is not part of the initial bundle.
|
|
307
|
-
|
|
308
|
-
### Special link embeds
|
|
309
|
-
|
|
310
|
-
External links use an ordered provider registry. YouTube watch, short, live, embed, and `youtu.be` links get a click-to-load, privacy-enhanced player. No YouTube iframe or SDK is loaded before activation; the preview uses only the supplied AppView thumbnail. Klipy's `static.klipy.com/ii/...` media links render GIFs or looping, muted video using Bluesky's media proxy, preserving the GIF description as alt text. Klipy media autoplays and loops without native video controls or a caption row. A small corner-overlay pause/play button controls the animation; pausing video preserves its current frame. With reduced motion enabled, it stays on the preview until explicitly played. Ordinary Klipy page links without a usable media URL remain link cards; no metadata API or API key is needed. Unknown URLs and playback failures fall back to the original link card.
|
|
311
|
-
|
|
312
|
-
Pass `embedProviders` to `PostContent` or `NestedComments` (it propagates to all replies and quotes). Omit it for YouTube + Klipy, or pass `[]` to disable special players. Add providers without modifying the renderer:
|
|
313
|
-
|
|
314
|
-
```ts
|
|
315
|
-
import { defaultExternalEmbedProviders, type ExternalEmbedProvider } from '@atmo-dev/kit/social';
|
|
316
|
-
|
|
317
|
-
const exampleProvider: ExternalEmbedProvider = {
|
|
318
|
-
id: 'example',
|
|
319
|
-
label: 'Example',
|
|
320
|
-
resolve(url) {
|
|
321
|
-
if (url.hostname !== 'example.com') return null;
|
|
322
|
-
const id = /^\/watch\/([a-zA-Z0-9_-]+)$/.exec(url.pathname)?.[1];
|
|
323
|
-
return id ? { type: 'iframe', src: `https://player.example.com/embed/${id}` } : null;
|
|
324
|
-
}
|
|
325
|
-
};
|
|
326
|
-
const embedProviders = [exampleProvider, ...defaultExternalEmbedProviders];
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
Providers are trusted app-owned, synchronous URL resolvers returning `iframe`, `video` (typed sources), or `image` descriptors—not HTML. Match exact hosts, validate IDs, and construct known player URLs. Media descriptors can set `autoplay: true` and videos can set `controls: false` for GIF-like playback; iframe players always require a click. The shared `ExternalEmbed` renderer owns playback policy, reduced-motion handling, sandboxed iframes, safe URL schemes, pause controls, source-change resets, and fallback cards. It is also exported for standalone use.
|
|
330
|
-
|
|
331
|
-
## Preferred social client
|
|
332
|
-
|
|
333
|
-
`SocialClientPicker` is a native form field group for Bluesky, mu, Blacksky, Witchsky, or a custom web client. Pass the viewer's saved `value` (a `PreferredSocialClient`, or `null` for Bluesky) and optionally `disabled`. It submits `client` and, for custom clients, `customUrl`. The app owns the form, save button, authentication, and feedback.
|
|
334
|
-
|
|
335
|
-
The dedicated public record is `social.atmo.actor.preferredClient/self`:
|
|
336
|
-
|
|
337
|
-
```json
|
|
338
|
-
{ "$type": "social.atmo.actor.preferredClient", "client": "mu" }
|
|
339
|
-
```
|
|
340
|
-
|
|
341
|
-
For a custom client, use `"client": "custom"` and `"customUrl": "https://example.com"`. This lexicon stores **only social-client selection**, not general preferences. Its schema is available at `@atmo-dev/kit/lexicons/social.atmo.actor.preferredClient.json`.
|
|
342
|
-
|
|
343
|
-
Framework-free helpers are exported from `@atmo-dev/kit/social` (and the main entry point):
|
|
344
|
-
|
|
345
|
-
- `PREFERRED_CLIENT_COLLECTION`, `PREFERRED_CLIENT_RKEY`, `PREFERRED_CLIENT_SCOPE` for repository IO and OAuth metadata/incremental permission requests. The scope grants create/update only on this collection; request it when the user chooses to authorize this setting, not during normal login or signup.
|
|
346
|
-
- `parsePreferredSocialClient(record)` validates repository records or form input; invalid, unknown or absent selections return `null`.
|
|
347
|
-
- `preferredSocialClientRecord(selection)` builds a validated record. Never write merely because a read failed; distinguish `RecordNotFound` from PDS/network failures, and use `swapRecord` to guard updates.
|
|
348
|
-
- `socialClientInfo(selection)`, `socialProfileUrl(actor, selection)`, `socialPostUrl(atUri, selection)`, and `socialHashtagUrl(tag, selection)` default to Bluesky. `socialPostUrl` accepts feed-post AT URIs and returns `null` for invalid ones. Custom URLs must be absolute HTTP(S), without credentials, query or fragment, and support `/profile/:actor` routes. Base paths are preserved.
|
|
349
|
-
|
|
350
|
-
Read the signed-in viewer's record from their PDS, include the parsed selection in viewer data, and pass it to `<UserProfile {profile} socialClient={viewer.preferredSocialClient} />`. The profile's external button and handle link use that client. Pass the same `socialClient` prop to `NestedComments` for author, reply, repost, like, and quoted-post links, including nested replies. Changing the selection updates these links without changing the stored post data. Bio hashtags also use the selected client. Supply an app-owned `mentionUrl` callback to `UserProfile` (for example, `(actor) => '/p/' + encodeURIComponent(actor)`) to route bio mentions locally in the same tab; without it they open the selected social client. Post-content facets retain their existing Bluesky links. Apps remain responsible for server-side authorization, IO, incremental authorization when access is missing, and invalidating viewer data after saves.
|
|
351
|
-
|
|
352
|
-
## Login identifiers
|
|
353
|
-
|
|
354
|
-
`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.
|
|
355
|
-
|
|
356
|
-
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.
|
|
357
|
-
|
|
358
|
-
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.
|
|
359
|
-
|
|
360
|
-
For other login entry points, import `normalizeIdentifier` and `isValidIdentifier`. Existing `normalizeHandle` and `isValidHandle` remain handle-only helpers; do not use them on DIDs.
|
|
361
|
-
|
|
362
|
-
## Legal pages and footer
|
|
363
|
-
|
|
364
|
-
`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.
|
|
365
|
-
|
|
366
|
-
```svelte
|
|
367
|
-
<script lang="ts">
|
|
368
|
-
import { LegalDocument, LegalFooter } from '@atmo-dev/kit';
|
|
369
|
-
import { privacy } from './reviewed-policies';
|
|
370
|
-
</script>
|
|
371
|
-
|
|
372
|
-
<LegalDocument document={privacy} />
|
|
373
|
-
<LegalFooter
|
|
374
|
-
links={{
|
|
375
|
-
terms: '/terms',
|
|
376
|
-
privacy: '/privacy',
|
|
377
|
-
imprint: '/imprint',
|
|
378
|
-
moderation: false,
|
|
379
|
-
report: '/report',
|
|
380
|
-
source: { label: 'Source code', href: 'https://github.com/example/project' }
|
|
381
|
-
}}
|
|
382
|
-
/>
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
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`.
|
|
386
|
-
|
|
387
|
-
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.
|
|
388
|
-
|
|
389
|
-
### Author and review documents
|
|
390
|
-
|
|
391
|
-
Import framework-free helpers from `@atmo-dev/kit/legal` in a script:
|
|
392
|
-
|
|
393
|
-
```ts
|
|
394
|
-
import { createLegalDrafts, getLegalDraftIssues } from '@atmo-dev/kit/legal';
|
|
395
|
-
|
|
396
|
-
const config = {
|
|
397
|
-
app: { name: 'Example', url: 'https://example.com', description: 'an event app' },
|
|
398
|
-
operator: { name: 'Operator', email: 'hello@example.com', address: [] },
|
|
399
|
-
privacy: [
|
|
400
|
-
{
|
|
401
|
-
id: 'storage',
|
|
402
|
-
title: 'Data storage',
|
|
403
|
-
paragraphs: [
|
|
404
|
-
'Describe actual data, purposes, recipients, retention, legal bases, and safeguards.'
|
|
405
|
-
]
|
|
406
|
-
}
|
|
407
|
-
],
|
|
408
|
-
moderation: [
|
|
409
|
-
{
|
|
410
|
-
id: 'reports',
|
|
411
|
-
title: 'Reporting',
|
|
412
|
-
paragraphs: ['Describe the working report channel and review procedure.']
|
|
413
|
-
}
|
|
414
|
-
]
|
|
415
|
-
};
|
|
416
|
-
console.error(getLegalDraftIssues(config));
|
|
417
|
-
console.log(JSON.stringify(createLegalDrafts(config), null, 2));
|
|
32
|
+
```sh
|
|
33
|
+
corepack pnpm install --frozen-lockfile
|
|
34
|
+
pnpm dev
|
|
418
35
|
```
|
|
419
36
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
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.
|
|
37
|
+
Browse `/components` for visual previews of every public component. Select one
|
|
38
|
+
for its interactive example at `/components/<name>`. The combined playgrounds
|
|
39
|
+
remain at `/`, `/examples`, `/navigation`, `/login`, and `/legal`.
|
|
425
40
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
```svelte
|
|
431
|
-
<ReportDialog
|
|
432
|
-
bind:open
|
|
433
|
-
isLoggedIn={viewer.isLoggedIn}
|
|
434
|
-
email="reports@example.com"
|
|
435
|
-
subjectUrl={canonicalUrl}
|
|
436
|
-
onsignin={showLogin}
|
|
437
|
-
submit={async (input) => {
|
|
438
|
-
try {
|
|
439
|
-
await sendReport({ subject, ...input });
|
|
440
|
-
} catch {
|
|
441
|
-
return { message: 'Reporting is unavailable. Please report by email.' };
|
|
442
|
-
}
|
|
443
|
-
}}
|
|
444
|
-
/>
|
|
445
|
-
```
|
|
41
|
+
Run `pnpm lint`, `pnpm check`, `pnpm test:ci`, and `pnpm build` to validate changes.
|
|
42
|
+
See [development](docs/development.md) for tests and package checks, and
|
|
43
|
+
[releases](.changeset/README.md) for publishing.
|
|
446
44
|
|
|
447
|
-
|
|
45
|
+
## License
|
|
448
46
|
|
|
449
|
-
|
|
47
|
+
[MIT](LICENSE).
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
return observeTheme(origin, sync);
|
|
42
42
|
});
|
|
43
43
|
const itemClass =
|
|
44
|
-
'min-h-11 w-full justify-start gap-3 px-3 text-sm text-
|
|
44
|
+
'min-h-11 w-full justify-start gap-3 px-3 text-sm text-base-950 dark:text-base-50 outline-none data-highlighted:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:data-highlighted:bg-base-950 data-highlighted:text-base-950 dark:data-highlighted:text-base-50';
|
|
45
45
|
</script>
|
|
46
46
|
|
|
47
47
|
<DropdownMenu.Root bind:open>
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
aria-label={label}
|
|
55
55
|
data-account-menu-trigger
|
|
56
56
|
class={cn(
|
|
57
|
-
'gap-3 p-1.5 data-[state=open]:bg-
|
|
57
|
+
'gap-3 p-1.5 data-[state=open]:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:data-[state=open]:bg-base-950',
|
|
58
58
|
expanded ? 'w-full justify-start px-2' : 'size-11 max-sm:size-10',
|
|
59
59
|
className
|
|
60
60
|
)}
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
<Avatar {name} src={avatar} size={30} aria-hidden="true" />
|
|
63
63
|
{#if expanded}
|
|
64
64
|
<span class="min-w-0 text-left">
|
|
65
|
-
<span class="block truncate text-sm text-
|
|
66
|
-
{#if handle}<span class="block truncate text-xs text-
|
|
65
|
+
<span class="block truncate text-sm text-base-950 dark:text-base-50">{name}</span>
|
|
66
|
+
{#if handle}<span class="block truncate text-xs text-base-600 dark:text-base-400"
|
|
67
67
|
>@{handle.replace(/^@/, '')}</span
|
|
68
68
|
>{/if}
|
|
69
69
|
</span>
|
|
@@ -81,19 +81,19 @@
|
|
|
81
81
|
aria-label={label}
|
|
82
82
|
data-slot="account-menu"
|
|
83
83
|
style={inherited}
|
|
84
|
-
class="z-60 w-56 max-w-[calc(100vw-1.5rem)] rounded-panel border border-
|
|
84
|
+
class="z-60 w-56 max-w-[calc(100vw-1.5rem)] rounded-panel border border-base-300 bg-base-50 p-1.5 font-sans text-base-950 outline-none dark:border-[color-mix(in_srgb,var(--base-800)_80%,var(--base-950))] dark:bg-base-925 dark:text-base-50"
|
|
85
85
|
>
|
|
86
86
|
<DropdownMenu.Item onSelect={onprofile} textValue="Go to profile">
|
|
87
87
|
{#snippet child({ props })}
|
|
88
88
|
<Button {...props} href={profileHref} variant="ghost" class={itemClass}>
|
|
89
|
-
<Icon img={UserRound} size={20} class="text-
|
|
89
|
+
<Icon img={UserRound} size={20} class="text-base-600 dark:text-base-400" />Go to profile
|
|
90
90
|
</Button>
|
|
91
91
|
{/snippet}
|
|
92
92
|
</DropdownMenu.Item>
|
|
93
93
|
<DropdownMenu.Item onSelect={onsignout} textValue="Sign out">
|
|
94
94
|
{#snippet child({ props })}
|
|
95
95
|
<Button {...props} variant="ghost" class={itemClass}>
|
|
96
|
-
<Icon img={LogOut} size={20} class="text-
|
|
96
|
+
<Icon img={LogOut} size={20} class="text-base-600 dark:text-base-400" />Sign out
|
|
97
97
|
</Button>
|
|
98
98
|
{/snippet}
|
|
99
99
|
</DropdownMenu.Item>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<span
|
|
21
21
|
{...rest}
|
|
22
22
|
class={cn(
|
|
23
|
-
'inline-grid size-(--ak-avatar-size) shrink-0 place-items-center overflow-hidden rounded-[45%_49%_44%_48%] border border-base-100 bg-accent-
|
|
23
|
+
'inline-grid size-(--ak-avatar-size) shrink-0 place-items-center overflow-hidden rounded-[45%_49%_44%_48%] border border-base-100 bg-accent-50 text-accent-700 dark:border-base-800 dark:bg-[color-mix(in_srgb,var(--accent-950)_65%,var(--base-950))] dark:text-accent-300',
|
|
24
24
|
className
|
|
25
25
|
)}
|
|
26
26
|
style:--ak-avatar-size={`${size}px`}
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
<span
|
|
13
13
|
{...rest}
|
|
14
14
|
class={cn(
|
|
15
|
-
'inline-flex w-fit items-center gap-1.5 rounded-
|
|
15
|
+
'inline-flex w-fit items-center gap-1.5 rounded-lg border px-2 py-0.75 text-[.6875rem] leading-normal font-medium',
|
|
16
16
|
variant === 'soft'
|
|
17
|
-
? 'border-
|
|
18
|
-
: 'border-
|
|
17
|
+
? 'border-accent-700/20 bg-accent-50 text-accent-700 dark:border-accent-300/20 dark:bg-[color-mix(in_srgb,var(--accent-950)_65%,var(--base-950))] dark:text-accent-300'
|
|
18
|
+
: 'border-base-300 bg-transparent text-base-600 dark:border-base-800 dark:text-base-400',
|
|
19
19
|
className
|
|
20
20
|
)}>{@render children?.()}</span
|
|
21
21
|
>
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
const variants = {
|
|
12
12
|
primary: 'bg-accent-400 text-base-950 hover:bg-accent-300 active:bg-accent-400',
|
|
13
13
|
secondary:
|
|
14
|
-
'border-
|
|
15
|
-
ghost:
|
|
16
|
-
|
|
14
|
+
'border-base-300 dark:border-[color-mix(in_srgb,var(--base-800)_80%,var(--base-950))] bg-base-50 dark:bg-base-900 text-base-950 dark:text-base-50 hover:border-base-300 dark:hover:border-base-800 hover:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:hover:bg-base-950 active:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:active:bg-base-950',
|
|
15
|
+
ghost:
|
|
16
|
+
'bg-transparent text-base-600 dark:text-base-400 hover:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:hover:bg-base-950 hover:text-base-950 dark:hover:text-base-50 active:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:active:bg-base-950',
|
|
17
|
+
link: 'min-h-0 rounded px-0 py-1 text-accent-700 dark:text-accent-300 hover:underline underline-offset-4'
|
|
17
18
|
};
|
|
18
19
|
const sizes = {
|
|
19
20
|
sm: 'min-h-8.5 gap-1.5 px-3 py-1.5 text-xs',
|
|
@@ -22,8 +22,38 @@
|
|
|
22
22
|
bind:this={ref}
|
|
23
23
|
data-slot="checkbox"
|
|
24
24
|
class={cn(
|
|
25
|
-
'm-0 inline-
|
|
25
|
+
'm-0 inline-grid size-4.5 shrink-0 cursor-pointer appearance-none place-content-center rounded-[5px] border border-base-300 bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] bg-none align-middle text-base-50 accent-accent-700 checked:border-accent-700 checked:bg-accent-700 indeterminate:border-accent-700 indeterminate:bg-accent-700 focus:ring-0 focus:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-45 dark:border-base-800 dark:bg-base-975 dark:text-base-950 dark:accent-accent-300 dark:checked:border-accent-300 dark:checked:bg-accent-300 dark:indeterminate:border-accent-300 dark:indeterminate:bg-accent-300',
|
|
26
26
|
focus,
|
|
27
27
|
className
|
|
28
28
|
)}
|
|
29
29
|
/>
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
input::before {
|
|
33
|
+
content: '';
|
|
34
|
+
width: 14px;
|
|
35
|
+
height: 14px;
|
|
36
|
+
background: currentColor;
|
|
37
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='m3 8 3 3 7-8' fill='none' stroke='black' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E")
|
|
38
|
+
center / contain no-repeat;
|
|
39
|
+
opacity: 0;
|
|
40
|
+
}
|
|
41
|
+
input:checked::before,
|
|
42
|
+
input:indeterminate::before {
|
|
43
|
+
opacity: 1;
|
|
44
|
+
}
|
|
45
|
+
input:indeterminate::before {
|
|
46
|
+
width: 9px;
|
|
47
|
+
height: 2px;
|
|
48
|
+
border-radius: 1px;
|
|
49
|
+
mask: none;
|
|
50
|
+
}
|
|
51
|
+
@media (forced-colors: active) {
|
|
52
|
+
input {
|
|
53
|
+
appearance: auto;
|
|
54
|
+
}
|
|
55
|
+
input::before {
|
|
56
|
+
display: none;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</style>
|