@atmo-dev/kit 0.8.0 → 0.9.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 CHANGED
@@ -16,6 +16,67 @@ Svelte 5 and Tailwind CSS 4 components for Atmo apps.
16
16
  for a separated, filled creation action in the rail, mobile bar or drawer. The
17
17
  existing `primary` flag alone retains its subtle accent/spacing behavior.
18
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
+
19
80
  ## AT record metadata (AT Tags)
20
81
 
21
82
  Use `AtTags` to let other clients discover which AT Protocol records and identities
@@ -0,0 +1,102 @@
1
+ <script lang="ts">
2
+ import { DropdownMenu } from 'bits-ui';
3
+ import { UserRound, LogOut } from '@sketchyicons/svelte';
4
+ import Avatar from './Avatar.svelte';
5
+ import Button from './Button.svelte';
6
+ import Icon from './Icon.svelte';
7
+ import { cn } from '../utils.js';
8
+ import { readTheme, observeTheme } from '../theme.js';
9
+
10
+ let {
11
+ name,
12
+ handle,
13
+ avatar,
14
+ profileHref,
15
+ onprofile,
16
+ onsignout,
17
+ expanded = false,
18
+ open = $bindable(false),
19
+ label = 'Account menu',
20
+ class: className
21
+ }: {
22
+ name: string;
23
+ handle?: string;
24
+ avatar?: string;
25
+ onsignout: () => void;
26
+ expanded?: boolean;
27
+ open?: boolean;
28
+ label?: string;
29
+ class?: string;
30
+ } & (
31
+ | { profileHref: string; onprofile?: () => void }
32
+ | { profileHref?: undefined; onprofile: () => void }
33
+ ) = $props();
34
+
35
+ let origin = $state<HTMLButtonElement | HTMLAnchorElement | null>(null);
36
+ let inherited = $state('');
37
+ $effect(() => {
38
+ if (!open || !origin) return;
39
+ const sync = () => (inherited = readTheme(origin!));
40
+ sync();
41
+ return observeTheme(origin, sync);
42
+ });
43
+ const itemClass =
44
+ 'min-h-11 w-full justify-start gap-3 px-3 text-sm text-ink outline-none data-highlighted:bg-recess data-highlighted:text-ink';
45
+ </script>
46
+
47
+ <DropdownMenu.Root bind:open>
48
+ <DropdownMenu.Trigger>
49
+ {#snippet child({ props })}
50
+ <Button
51
+ {...props}
52
+ bind:ref={origin}
53
+ variant="ghost"
54
+ aria-label={label}
55
+ data-account-menu-trigger
56
+ class={cn(
57
+ 'gap-3 p-1.5 data-[state=open]:bg-recess',
58
+ expanded ? 'w-full justify-start px-2' : 'size-11 max-sm:size-10',
59
+ className
60
+ )}
61
+ >
62
+ <Avatar {name} src={avatar} size={30} aria-hidden="true" />
63
+ {#if expanded}
64
+ <span class="min-w-0 text-left">
65
+ <span class="block truncate text-sm text-ink">{name}</span>
66
+ {#if handle}<span class="block truncate text-xs text-muted"
67
+ >@{handle.replace(/^@/, '')}</span
68
+ >{/if}
69
+ </span>
70
+ {/if}
71
+ </Button>
72
+ {/snippet}
73
+ </DropdownMenu.Trigger>
74
+ <DropdownMenu.Portal>
75
+ <DropdownMenu.Content
76
+ side="top"
77
+ align="start"
78
+ sideOffset={8}
79
+ collisionPadding={12}
80
+ loop
81
+ aria-label={label}
82
+ data-slot="account-menu"
83
+ style={inherited}
84
+ class="z-60 w-56 max-w-[calc(100vw-1.5rem)] rounded-panel border border-line bg-page p-1.5 font-sans text-ink outline-none"
85
+ >
86
+ <DropdownMenu.Item onSelect={onprofile} textValue="Go to profile">
87
+ {#snippet child({ props })}
88
+ <Button {...props} href={profileHref} variant="ghost" class={itemClass}>
89
+ <Icon img={UserRound} size={20} class="text-muted" />Go to profile
90
+ </Button>
91
+ {/snippet}
92
+ </DropdownMenu.Item>
93
+ <DropdownMenu.Item onSelect={onsignout} textValue="Sign out">
94
+ {#snippet child({ props })}
95
+ <Button {...props} variant="ghost" class={itemClass}>
96
+ <Icon img={LogOut} size={20} class="text-muted" />Sign out
97
+ </Button>
98
+ {/snippet}
99
+ </DropdownMenu.Item>
100
+ </DropdownMenu.Content>
101
+ </DropdownMenu.Portal>
102
+ </DropdownMenu.Root>
@@ -0,0 +1,19 @@
1
+ type $$ComponentProps = {
2
+ name: string;
3
+ handle?: string;
4
+ avatar?: string;
5
+ onsignout: () => void;
6
+ expanded?: boolean;
7
+ open?: boolean;
8
+ label?: string;
9
+ class?: string;
10
+ } & ({
11
+ profileHref: string;
12
+ onprofile?: () => void;
13
+ } | {
14
+ profileHref?: undefined;
15
+ onprofile: () => void;
16
+ });
17
+ declare const AccountMenu: import("svelte").Component<$$ComponentProps, {}, "open">;
18
+ type AccountMenu = ReturnType<typeof AccountMenu>;
19
+ export default AccountMenu;
@@ -61,7 +61,7 @@
61
61
  {...rest}
62
62
  bind:this={ref}
63
63
  href={disabled || loading ? undefined : href}
64
- role={disabled || loading ? 'link' : undefined}
64
+ role={disabled || loading ? 'link' : rest.role}
65
65
  class={classes}
66
66
  data-slot="button"
67
67
  data-variant={variant}
@@ -6,7 +6,7 @@
6
6
  type ExternalEmbedProvider,
7
7
  type ExternalPostEmbed
8
8
  } from '../social/external-embeds.js';
9
- import { safeWebUrl } from '../social/richtext.js';
9
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
10
10
  import { focus } from '../styles.js';
11
11
  import Icon from './Icon.svelte';
12
12
  import LinkCard from './LinkCard.svelte';
@@ -2,7 +2,7 @@
2
2
  import type { HTMLAnchorAttributes } from 'svelte/elements';
3
3
  import { cn } from '../utils.js';
4
4
  import { focus } from '../styles.js';
5
- import { safeWebUrl } from '../social/richtext.js';
5
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
6
6
  let {
7
7
  href,
8
8
  meta,
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { PostData } from '../social/posts.js';
3
3
  import type { ExternalEmbedProvider } from '../social/external-embeds.js';
4
- import { safeWebUrl } from '../social/richtext.js';
4
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
5
5
  import { socialPostUrl, socialProfileUrl } from '../social/urls.js';
6
6
  import type { PreferredSocialClient } from '../social/preferred-client.js';
7
7
  import { Heart, MessageCircle, Repeat2 } from '@sketchyicons/svelte';
@@ -5,7 +5,7 @@
5
5
  import { socialPostUrl } from '../social/urls.js';
6
6
  import { focus } from '../styles.js';
7
7
  import Icon from './Icon.svelte';
8
- import { safeWebUrl } from '../social/richtext.js';
8
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
9
9
  import RichText from './RichText.svelte';
10
10
  import type { ExternalEmbedProvider } from '../social/external-embeds.js';
11
11
  import ExternalEmbed from './ExternalEmbed.svelte';
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { safeWebUrl } from '../social/richtext.js';
2
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
3
3
  import Icon from './Icon.svelte';
4
4
  import { ArrowUpRight } from '@sketchyicons/svelte';
5
5
  let {
@@ -1,3 +1,4 @@
1
+ import { safeHttpUrl } from '../../urls.js';
1
2
  /** Same-origin absolute paths and web URLs only. Never executable, credentialed,
2
3
  * protocol-relative, or backslash-normalized URLs. Mail is allowed explicitly. */
3
4
  export function safeLegalHref(value, allowMail = false) {
@@ -9,15 +10,7 @@ export function safeLegalHref(value, allowMail = false) {
9
10
  return value;
10
11
  if (allowMail && /^mailto:[^@<>?&#%]+@[^@<>?&#%]+\.[^@<>?&#%]+$/.test(value))
11
12
  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;
13
+ return safeHttpUrl(value);
21
14
  }
22
15
  const labels = {
23
16
  terms: 'Terms',
@@ -1,14 +1,23 @@
1
1
  <script lang="ts">
2
2
  import { Icon } from '../../index.js';
3
3
  import { Link } from '@sketchyicons/svelte';
4
+ import { safeHttpUrl } from '../../urls.js';
4
5
  let { uris = [] }: { uris?: Array<{ uri: string; name?: string }> } = $props();
6
+ let links = $derived(
7
+ uris.flatMap((link) => {
8
+ const uri = safeHttpUrl(link?.uri);
9
+ return uri ? [{ ...link, uri }] : [];
10
+ })
11
+ );
5
12
  </script>
6
13
 
7
- {#if uris.length}
14
+ {#if links.length}
8
15
  <div>
9
16
  <p class="mb-3 text-xs font-medium text-muted">Links</p>
10
17
  <div class="space-y-3">
11
- {#each uris as link (link.name + link.uri)}
18
+ <!-- These rows are stateless; equivalent URLs and exact duplicate entries are valid. -->
19
+ <!-- eslint-disable-next-line svelte/require-each-key -->
20
+ {#each links as link}
12
21
  <a
13
22
  href={link.uri}
14
23
  target="_blank"
@@ -1,34 +1,38 @@
1
1
  <script lang="ts">
2
2
  import { ArrowUpRight } from '@sketchyicons/svelte';
3
3
  import { Button, Icon } from '../../index.js';
4
+ import { safeHttpUrl } from '../../urls.js';
4
5
 
5
6
  // Shown in place of the RSVP controls for imported events whose host opted out
6
7
  // of atmo RSVPs (additionalData.externalSource.rsvpMode === 'external_only').
7
8
  // Directs attendees to the original event page instead.
8
9
  let { url }: { url: string } = $props();
9
10
 
11
+ let href = $derived(safeHttpUrl(url));
10
12
  let host = $derived.by(() => {
11
13
  try {
12
- return new URL(url).hostname.replace(/^www\./, '');
14
+ return new URL(href ?? '').hostname.replace(/^www\./, '');
13
15
  } catch {
14
16
  return null;
15
17
  }
16
18
  });
17
19
  </script>
18
20
 
19
- <div
20
- class="mt-8 mb-2 flex flex-col gap-3 rounded-2xl border border-base-200 bg-base-100 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-base-800 dark:bg-base-950/50"
21
- >
22
- <div class="min-w-0">
23
- <p class="text-sm font-semibold text-base-900 dark:text-base-50">RSVP on the original page</p>
24
- <p class="mt-0.5 text-xs text-base-600 dark:text-base-400">
25
- {host
26
- ? `This event is hosted on ${host}. Head there to RSVP.`
27
- : 'This event is hosted elsewhere. Open the original page to RSVP.'}
28
- </p>
21
+ {#if href}
22
+ <div
23
+ class="mt-8 mb-2 flex flex-col gap-3 rounded-2xl border border-base-200 bg-base-100 p-4 sm:flex-row sm:items-center sm:justify-between dark:border-base-800 dark:bg-base-950/50"
24
+ >
25
+ <div class="min-w-0">
26
+ <p class="text-sm font-semibold text-base-900 dark:text-base-50">RSVP on the original page</p>
27
+ <p class="mt-0.5 text-xs text-base-600 dark:text-base-400">
28
+ {host
29
+ ? `This event is hosted on ${host}. Head there to RSVP.`
30
+ : 'This event is hosted elsewhere. Open the original page to RSVP.'}
31
+ </p>
32
+ </div>
33
+ <Button {href} target="_blank" rel="noopener noreferrer" class="shrink-0">
34
+ {host ? `RSVP on ${host}` : 'Open original event'}
35
+ <Icon img={ArrowUpRight} size={16} class="shrink-0" />
36
+ </Button>
29
37
  </div>
30
- <Button href={url} target="_blank" rel="noopener noreferrer" class="shrink-0">
31
- {host ? `RSVP on ${host}` : 'Open original event'}
32
- <Icon img={ArrowUpRight} size={16} class="shrink-0" />
33
- </Button>
34
- </div>
38
+ {/if}
@@ -1,3 +1,4 @@
1
+ import { safeHttpUrl as webUrl } from '../../urls.js';
1
2
  function text(record, key) {
2
3
  const value = record[key];
3
4
  return typeof value === 'string' ? value.trim() || undefined : undefined;
@@ -20,17 +21,6 @@ function coordinateLabel({ lat, lng }) {
20
21
  function mapsUrl(query) {
21
22
  return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`;
22
23
  }
23
- function webUrl(uri) {
24
- try {
25
- const url = new URL(uri);
26
- if (url.protocol === 'https:' || url.protocol === 'http:')
27
- return url.href;
28
- }
29
- catch {
30
- // Invalid URLs render as non-interactive location text.
31
- }
32
- return undefined;
33
- }
34
24
  function formatLocation(value) {
35
25
  if (!value || typeof value !== 'object')
36
26
  return null;
package/dist/index.d.ts CHANGED
@@ -54,6 +54,7 @@ export type { IconNode, SketchyIconProps } from '@sketchyicons/svelte';
54
54
  export { default as IconRail } from './components/IconRail.svelte';
55
55
  export type { IconRailItem } from './components/IconRail.svelte';
56
56
  export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
57
+ export { default as AccountMenu } from './components/AccountMenu.svelte';
57
58
  export { default as LoginForm } from './components/login/LoginForm.svelte';
58
59
  export type { LoginFormProps } from './components/login/LoginForm.svelte';
59
60
  export { default as LoginDialog } from './components/login/LoginDialog.svelte';
@@ -76,6 +77,7 @@ export { default as InlineEditor } from './components/InlineEditor.svelte';
76
77
  export { default as PostComposer } from './components/PostComposer.svelte';
77
78
  export { default as NestedComments } from './components/NestedComments.svelte';
78
79
  export { default as PostContent } from './components/PostContent.svelte';
80
+ export { safeHttpUrl, safeLinkHref } from './urls.js';
79
81
  export { default as RichText } from './components/RichText.svelte';
80
82
  export type { RichTextProps } from './components/RichText.svelte';
81
83
  export type { RichTextDocument, RichTextLinkOptions } from './richtext/document.js';
package/dist/index.js CHANGED
@@ -34,6 +34,7 @@ export { default as Tabs } from './components/Tabs.svelte';
34
34
  export { default as Icon } from './components/Icon.svelte';
35
35
  export { default as IconRail } from './components/IconRail.svelte';
36
36
  export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
37
+ export { default as AccountMenu } from './components/AccountMenu.svelte';
37
38
  export { default as LoginForm } from './components/login/LoginForm.svelte';
38
39
  export { default as LoginDialog } from './components/login/LoginDialog.svelte';
39
40
  export { default as SignupPermissions } from './components/login/SignupPermissions.svelte';
@@ -51,6 +52,7 @@ export { default as InlineEditor } from './components/InlineEditor.svelte';
51
52
  export { default as PostComposer } from './components/PostComposer.svelte';
52
53
  export { default as NestedComments } from './components/NestedComments.svelte';
53
54
  export { default as PostContent } from './components/PostContent.svelte';
55
+ export { safeHttpUrl, safeLinkHref } from './urls.js';
54
56
  export { default as RichText } from './components/RichText.svelte';
55
57
  export { default as ExternalEmbed } from './components/ExternalEmbed.svelte';
56
58
  export { defaultExternalEmbedProviders, youtubeEmbedProvider, klipyEmbedProvider, resolveExternalEmbed } from './social/external-embeds.js';
@@ -1,4 +1,6 @@
1
1
  import type { PreferredSocialClient } from '../social/preferred-client.js';
2
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
3
+ export { safeWebUrl };
2
4
  /**
3
5
  * Parsed rich text. It is plain JSON, so it can be produced on the server and sent in page
4
6
  * data. Renderers still treat it as untrusted and validate every node and destination.
@@ -61,7 +63,6 @@ export type RichTextLinkOptions = {
61
63
  /** App-owned mention destination; defaults to the selected social client. */
62
64
  mentionUrl?: (identifier: string) => string;
63
65
  };
64
- export declare function safeWebUrl(url: unknown): string | undefined;
65
66
  /** Only app-relative paths or HTTP(S), including for app-owned mention callbacks. */
66
67
  export declare function mentionHref(actor: unknown, options: RichTextLinkOptions): string | undefined;
67
68
  export declare function tagHref(tag: unknown, options: RichTextLinkOptions): string | undefined;
@@ -1,18 +1,8 @@
1
1
  import { isActorIdentifier } from '@atcute/lexicons/syntax';
2
2
  import { socialHashtagUrl, socialProfileUrl } from '../social/urls.js';
3
- export function safeWebUrl(url) {
4
- if (typeof url !== 'string' || !url)
5
- return undefined;
6
- try {
7
- const parsed = new URL(url);
8
- return ['https:', 'http:'].includes(parsed.protocol) && !parsed.username && !parsed.password
9
- ? parsed.href
10
- : undefined;
11
- }
12
- catch {
13
- return undefined;
14
- }
15
- }
3
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
4
+ // Backwards-compatible export for existing richtext and social consumers.
5
+ export { safeWebUrl };
16
6
  /** Only app-relative paths or HTTP(S), including for app-owned mention callbacks. */
17
7
  export function mentionHref(actor, options) {
18
8
  if (typeof actor !== 'string' || !isActorIdentifier(actor))
@@ -1,4 +1,4 @@
1
- import { safeWebUrl } from './richtext.js';
1
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
2
2
  function youtubeStart(value) {
3
3
  if (!value)
4
4
  return 0;
@@ -1,5 +1,5 @@
1
1
  import { detectTokens } from '../richtext/detect.js';
2
- import { safeWebUrl } from './richtext.js';
2
+ import { safeHttpUrl as safeWebUrl } from '../urls.js';
3
3
  import { socialHashtagUrl, socialProfileUrl } from './urls.js';
4
4
  /** Only web URLs; never accept executable schemes or disguised credentials. */
5
5
  export function safeProfileUrl(value) {
package/dist/urls.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Untrusted external destinations: absolute HTTP(S) URLs without credentials.
2
+ * This validates browser destinations, not hosts for server-side fetching (SSRF).
3
+ */
4
+ export declare function safeHttpUrl(value: unknown): string | undefined;
5
+ /** Navigation also supports root-relative app paths, fragments and mail/phone actions.
6
+ * Use safeHttpUrl instead when a record field must contain an external website.
7
+ * Invalid destinations return undefined so renderers omit href rather than falling back to it.
8
+ */
9
+ export declare function safeLinkHref(value: unknown): string | undefined;
package/dist/urls.js ADDED
@@ -0,0 +1,39 @@
1
+ /** Untrusted external destinations: absolute HTTP(S) URLs without credentials.
2
+ * This validates browser destinations, not hosts for server-side fetching (SSRF).
3
+ */
4
+ export function safeHttpUrl(value) {
5
+ if (typeof value !== 'string' || !value)
6
+ return undefined;
7
+ try {
8
+ const url = new URL(value);
9
+ return (url.protocol === 'https:' || url.protocol === 'http:') && !url.username && !url.password
10
+ ? url.href
11
+ : undefined;
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ /** Navigation also supports root-relative app paths, fragments and mail/phone actions.
18
+ * Use safeHttpUrl instead when a record field must contain an external website.
19
+ * Invalid destinations return undefined so renderers omit href rather than falling back to it.
20
+ */
21
+ export function safeLinkHref(value) {
22
+ const http = safeHttpUrl(value);
23
+ if (http)
24
+ return http;
25
+ if (typeof value !== 'string' || !value)
26
+ return undefined;
27
+ // Never normalize an ambiguous local/contact link into a different destination.
28
+ if (Array.from(value).some((c) => c === '\\' || c.charCodeAt(0) <= 0x20 || c.charCodeAt(0) === 0x7f))
29
+ return undefined;
30
+ if (value.startsWith('#') || (value.startsWith('/') && !value.startsWith('//')))
31
+ return value;
32
+ try {
33
+ const url = new URL(value);
34
+ return url.protocol === 'mailto:' || url.protocol === 'tel:' ? url.href : undefined;
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atmo-dev/kit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "The shared UI foundation for Atmo apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -56,6 +56,10 @@
56
56
  "types": "./dist/media.d.ts",
57
57
  "default": "./dist/media.js"
58
58
  },
59
+ "./urls": {
60
+ "types": "./dist/urls.d.ts",
61
+ "default": "./dist/urls.js"
62
+ },
59
63
  "./richtext": {
60
64
  "types": "./dist/richtext.d.ts",
61
65
  "default": "./dist/richtext.js"