@atmo-dev/kit 0.8.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.
Files changed (94) hide show
  1. package/README.md +27 -368
  2. package/dist/components/AccountMenu.svelte +102 -0
  3. package/dist/components/AccountMenu.svelte.d.ts +19 -0
  4. package/dist/components/Avatar.svelte +1 -1
  5. package/dist/components/Badge.svelte +3 -3
  6. package/dist/components/Button.svelte +5 -4
  7. package/dist/components/Checkbox.svelte +31 -1
  8. package/dist/{events → components}/DatePicker.svelte +3 -3
  9. package/dist/components/ExternalEmbed.svelte +10 -7
  10. package/dist/components/GrainAlbumPicker.svelte +18 -9
  11. package/dist/components/GrainAlbumView.svelte +15 -6
  12. package/dist/components/Heading.svelte +1 -1
  13. package/dist/components/IconRail.svelte +11 -9
  14. package/dist/components/InlineEditor.svelte +3 -3
  15. package/dist/components/InsetImage.svelte +1 -1
  16. package/dist/components/Label.svelte +1 -1
  17. package/dist/components/LinkCard.svelte +7 -5
  18. package/dist/components/Modal.svelte +3 -3
  19. package/dist/components/NestedComments.svelte +10 -7
  20. package/dist/components/PhotoGrid.svelte +1 -1
  21. package/dist/components/Popover.svelte +1 -1
  22. package/dist/components/PostComposer.svelte +2 -2
  23. package/dist/components/PostContent.svelte +12 -10
  24. package/dist/components/PostVideo.svelte +2 -2
  25. package/dist/components/RichText.svelte +19 -7
  26. package/dist/components/Select.svelte +5 -2
  27. package/dist/components/Select.svelte.d.ts +2 -0
  28. package/dist/components/SocialClientPicker.svelte +1 -1
  29. package/dist/components/StreamplacePicker.svelte +1 -1
  30. package/dist/components/Subheading.svelte +4 -2
  31. package/dist/components/Surface.svelte +3 -2
  32. package/dist/components/Tabs.svelte +2 -2
  33. package/dist/{events → components}/TimePicker.svelte +10 -7
  34. package/dist/{events → components}/TimezonePicker.svelte +15 -7
  35. package/dist/components/ToggleGroup.svelte +1 -1
  36. package/dist/components/ToggleGroupItem.svelte +1 -1
  37. package/dist/components/UserProfile.svelte +16 -9
  38. package/dist/components/WorkspaceSidebar.svelte +1 -1
  39. package/dist/components/legal/LegalDocument.svelte +9 -3
  40. package/dist/components/legal/LegalFooter.svelte +2 -2
  41. package/dist/components/legal/links.js +2 -9
  42. package/dist/components/login/ActorSearchField.svelte +10 -6
  43. package/dist/components/login/AtmosphereApps.svelte +1 -1
  44. package/dist/components/login/LoginForm.svelte +15 -9
  45. package/dist/components/login/PermissionFields.svelte +9 -2
  46. package/dist/components/login/RecentAccounts.svelte +5 -3
  47. package/dist/events/EventAttendees.svelte +9 -3
  48. package/dist/events/EventCard.svelte +7 -5
  49. package/dist/events/EventComments.svelte +3 -3
  50. package/dist/events/EventEditor.svelte +20 -9
  51. package/dist/events/EventRsvp.svelte +10 -6
  52. package/dist/events/EventView.svelte +21 -7
  53. package/dist/events/StarterPackButton.svelte +3 -3
  54. package/dist/events/ThemePicker.svelte +7 -7
  55. package/dist/events/ThumbnailPresets.svelte +1 -1
  56. package/dist/events/VodPlayer.svelte +1 -1
  57. package/dist/events/editor/LinksSection.svelte +7 -3
  58. package/dist/events/editor/LocationSection.svelte +1 -1
  59. package/dist/events/editor/RecurringModal.svelte +6 -4
  60. package/dist/events/editor/ThemeSection.svelte +3 -1
  61. package/dist/events/event-view/EventDateBlock.svelte +3 -3
  62. package/dist/events/event-view/EventHostedBy.svelte +6 -2
  63. package/dist/events/event-view/EventLinksList.svelte +13 -4
  64. package/dist/events/event-view/EventLocationBlock.svelte +9 -5
  65. package/dist/events/event-view/EventLocationMap.svelte +4 -2
  66. package/dist/events/event-view/ExternalRsvpNotice.svelte +20 -16
  67. package/dist/events/event-view/locations.js +1 -11
  68. package/dist/events/schedule/ConferenceTimetable.svelte +7 -7
  69. package/dist/events/schedule/DaySchedule.svelte +2 -2
  70. package/dist/events/schedule/ScheduleEventCell.svelte +2 -2
  71. package/dist/events/themes/Matrix.svelte +12 -6
  72. package/dist/events.d.ts +5 -5
  73. package/dist/events.js +6 -5
  74. package/dist/index.d.ts +7 -0
  75. package/dist/index.js +7 -0
  76. package/dist/richtext/document.d.ts +2 -1
  77. package/dist/richtext/document.js +3 -13
  78. package/dist/social/external-embeds.js +1 -1
  79. package/dist/social/mention-suggestions.js +4 -4
  80. package/dist/social/profile.js +1 -1
  81. package/dist/styles.d.ts +2 -2
  82. package/dist/styles.js +2 -2
  83. package/dist/theme.css +23 -44
  84. package/dist/theme.js +4 -12
  85. package/dist/urls.d.ts +9 -0
  86. package/dist/urls.js +39 -0
  87. package/package.json +5 -1
  88. /package/dist/{events → components}/DatePicker.svelte.d.ts +0 -0
  89. /package/dist/{events → components}/DateTimePicker.svelte +0 -0
  90. /package/dist/{events → components}/DateTimePicker.svelte.d.ts +0 -0
  91. /package/dist/{events → components}/ImageDropper.svelte +0 -0
  92. /package/dist/{events → components}/ImageDropper.svelte.d.ts +0 -0
  93. /package/dist/{events → components}/TimePicker.svelte.d.ts +0 -0
  94. /package/dist/{events → components}/TimezonePicker.svelte.d.ts +0 -0
@@ -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;
@@ -205,7 +205,7 @@
205
205
  {/if}
206
206
  </div>
207
207
 
208
- <p class="mb-4 text-sm text-muted">All times shown in {tzLabel}.</p>
208
+ <p class="mb-4 text-sm text-base-600 dark:text-base-400">All times shown in {tzLabel}.</p>
209
209
 
210
210
  {#if upNextAll.length > 0}
211
211
  <div class="mb-8">
@@ -235,7 +235,7 @@
235
235
  {ev.title}
236
236
  </p>
237
237
  {#if subtitle(ev)}
238
- <p class="mt-0.5 truncate text-xs text-muted">
238
+ <p class="mt-0.5 truncate text-xs text-base-600 dark:text-base-400">
239
239
  {subtitle(ev)}
240
240
  </p>
241
241
  {/if}
@@ -243,12 +243,12 @@
243
243
  <span
244
244
  class="flex shrink-0 items-center gap-1.5 text-xs whitespace-nowrap {w.soon
245
245
  ? 'font-semibold text-accent-600 dark:text-accent-400'
246
- : 'text-muted'}"
246
+ : 'text-base-600 dark:text-base-400'}"
247
247
  >
248
248
  {#if rsvpStatuses[ev.uri] === 'going'}
249
- <Icon img={Check} size={14} class="text-action" />
249
+ <Icon img={Check} size={14} class="text-accent-700 dark:text-accent-300" />
250
250
  {:else}
251
- <Icon img={Star} size={14} class="text-action" />
251
+ <Icon img={Star} size={14} class="text-accent-700 dark:text-accent-300" />
252
252
  {/if}
253
253
  {w.text}
254
254
  </span>
@@ -273,7 +273,7 @@
273
273
  <div class="mb-6 space-y-3">
274
274
  {#if dayGroups.length > 1}
275
275
  <div class="flex items-center gap-3">
276
- <span class="w-14 text-xs text-muted">Days</span>
276
+ <span class="w-14 text-xs text-base-600 dark:text-base-400">Days</span>
277
277
  <ToggleGroup
278
278
  type="single"
279
279
  bind:value={
@@ -293,7 +293,7 @@
293
293
  {/if}
294
294
  {#if loggedIn}
295
295
  <div class="flex items-center gap-3">
296
- <span class="w-14 text-xs text-muted">Events</span>
296
+ <span class="w-14 text-xs text-base-600 dark:text-base-400">Events</span>
297
297
  <ToggleGroup
298
298
  type="single"
299
299
  bind:value={
@@ -97,7 +97,7 @@
97
97
  onclick={() => (roomMenuOpen = false)}
98
98
  ></button>
99
99
  <ul
100
- class="absolute inset-x-0 top-full z-[10000] max-h-64 overflow-auto border-b border-line bg-page"
100
+ class="absolute inset-x-0 top-full z-[10000] max-h-64 overflow-auto border-b border-base-300 bg-base-50 dark:border-[color-mix(in_srgb,var(--base-800)_80%,var(--base-950))] dark:bg-base-925"
101
101
  >
102
102
  {#each rooms as room, i (room)}
103
103
  <li>
@@ -209,7 +209,7 @@
209
209
  >
210
210
  {#each rooms as room (room)}
211
211
  <div
212
- class="px-2 py-2.5 text-center text-xs font-semibold tracking-wide text-muted uppercase"
212
+ class="px-2 py-2.5 text-center text-xs font-semibold tracking-wide text-base-600 uppercase dark:text-base-400"
213
213
  >
214
214
  {room}
215
215
  </div>
@@ -89,7 +89,7 @@
89
89
  <Modal bind:open={modalOpen} title={event.title}>
90
90
  <div class="overflow-hidden">
91
91
  {#if event.start}
92
- <p class="mt-1 text-sm text-muted">
92
+ <p class="mt-1 text-sm text-base-600 dark:text-base-400">
93
93
  {formatTime(event.start, tz)}{event.end ? ` – ${formatTime(event.end, tz)}` : ''}
94
94
  </p>
95
95
  {/if}
@@ -107,7 +107,7 @@
107
107
  {/if}
108
108
 
109
109
  {#if event.description}
110
- <p class="mt-3 line-clamp-3 text-sm break-words text-muted">
110
+ <p class="mt-3 line-clamp-3 text-sm break-words text-base-600 dark:text-base-400">
111
111
  {event.description}
112
112
  </p>
113
113
  {/if}
@@ -37,23 +37,28 @@
37
37
  );
38
38
 
39
39
  const accentColor = getComputedStyle(document.documentElement)
40
- .getPropertyValue('--color-accent-500')
40
+ .getPropertyValue('--accent-500')
41
41
  .trim();
42
42
 
43
+ const root = document.documentElement;
44
+ const systemScheme = window.matchMedia('(prefers-color-scheme: dark)');
43
45
  let bgColor = '';
44
46
  function readBgColor() {
45
- const isDark = document.documentElement.classList.contains('dark');
46
- bgColor = getComputedStyle(document.documentElement)
47
- .getPropertyValue(isDark ? '--color-base-900' : '--color-base-50')
47
+ const isDark =
48
+ root.matches('.dark, [data-theme="dark"]') ||
49
+ (!root.matches('.light, [data-theme="light"]') && systemScheme.matches);
50
+ bgColor = getComputedStyle(root)
51
+ .getPropertyValue(isDark ? '--base-900' : '--base-50')
48
52
  .trim();
49
53
  }
50
54
  readBgColor();
51
55
 
52
56
  const themeObserver = new MutationObserver(readBgColor);
53
- themeObserver.observe(document.documentElement, {
57
+ themeObserver.observe(root, {
54
58
  attributes: true,
55
- attributeFilter: ['class']
59
+ attributeFilter: ['class', 'data-theme', 'style']
56
60
  });
61
+ systemScheme.addEventListener('change', readBgColor);
57
62
 
58
63
  let lastResize = canvas.width;
59
64
  let lastTime = performance.now();
@@ -144,6 +149,7 @@
144
149
  cancelAnimationFrame(animId);
145
150
  window.removeEventListener('resize', resize);
146
151
  themeObserver.disconnect();
152
+ systemScheme.removeEventListener('change', readBgColor);
147
153
  };
148
154
  });
149
155
  </script>
package/dist/events.d.ts CHANGED
@@ -12,8 +12,8 @@ export { default as VodPlayer } from './events/VodPlayer.svelte';
12
12
  export type { VodPlayerApi } from './events/VodPlayer.svelte';
13
13
  export { default as VodTranscript } from './events/VodTranscript.svelte';
14
14
  export { default as ConferenceTimetable } from './events/schedule/ConferenceTimetable.svelte';
15
- export { default as DatePicker } from './events/DatePicker.svelte';
16
- export { default as TimePicker } from './events/TimePicker.svelte';
17
- export { default as DateTimePicker } from './events/DateTimePicker.svelte';
18
- export { default as TimezonePicker } from './events/TimezonePicker.svelte';
19
- export { default as ImageDropper } from './events/ImageDropper.svelte';
15
+ export { default as DatePicker } from './components/DatePicker.svelte';
16
+ export { default as TimePicker } from './components/TimePicker.svelte';
17
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
18
+ export { default as TimezonePicker } from './components/TimezonePicker.svelte';
19
+ export { default as ImageDropper } from './components/ImageDropper.svelte';
package/dist/events.js CHANGED
@@ -10,8 +10,9 @@ export { default as StarterPackButton } from './events/StarterPackButton.svelte'
10
10
  export { default as VodPlayer } from './events/VodPlayer.svelte';
11
11
  export { default as VodTranscript } from './events/VodTranscript.svelte';
12
12
  export { default as ConferenceTimetable } from './events/schedule/ConferenceTimetable.svelte';
13
- export { default as DatePicker } from './events/DatePicker.svelte';
14
- export { default as TimePicker } from './events/TimePicker.svelte';
15
- export { default as DateTimePicker } from './events/DateTimePicker.svelte';
16
- export { default as TimezonePicker } from './events/TimezonePicker.svelte';
17
- export { default as ImageDropper } from './events/ImageDropper.svelte';
13
+ // Compatibility aliases; general-purpose controls are owned by the main Kit entry.
14
+ export { default as DatePicker } from './components/DatePicker.svelte';
15
+ export { default as TimePicker } from './components/TimePicker.svelte';
16
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
17
+ export { default as TimezonePicker } from './components/TimezonePicker.svelte';
18
+ export { default as ImageDropper } from './components/ImageDropper.svelte';
package/dist/index.d.ts CHANGED
@@ -5,6 +5,11 @@ export { default as Input } from './components/Input.svelte';
5
5
  export type { InputProps } from './components/Input.svelte';
6
6
  export { default as Textarea } from './components/Textarea.svelte';
7
7
  export { default as Select } from './components/Select.svelte';
8
+ export { default as DatePicker } from './components/DatePicker.svelte';
9
+ export { default as TimePicker } from './components/TimePicker.svelte';
10
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
11
+ export { default as TimezonePicker } from './components/TimezonePicker.svelte';
12
+ export { default as ImageDropper } from './components/ImageDropper.svelte';
8
13
  export { default as SocialClientPicker } from './components/SocialClientPicker.svelte';
9
14
  export type { SocialClientPickerProps } from './components/SocialClientPicker.svelte';
10
15
  export * from './social.js';
@@ -54,6 +59,7 @@ export type { IconNode, SketchyIconProps } from '@sketchyicons/svelte';
54
59
  export { default as IconRail } from './components/IconRail.svelte';
55
60
  export type { IconRailItem } from './components/IconRail.svelte';
56
61
  export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
62
+ export { default as AccountMenu } from './components/AccountMenu.svelte';
57
63
  export { default as LoginForm } from './components/login/LoginForm.svelte';
58
64
  export type { LoginFormProps } from './components/login/LoginForm.svelte';
59
65
  export { default as LoginDialog } from './components/login/LoginDialog.svelte';
@@ -76,6 +82,7 @@ export { default as InlineEditor } from './components/InlineEditor.svelte';
76
82
  export { default as PostComposer } from './components/PostComposer.svelte';
77
83
  export { default as NestedComments } from './components/NestedComments.svelte';
78
84
  export { default as PostContent } from './components/PostContent.svelte';
85
+ export { safeHttpUrl, safeLinkHref } from './urls.js';
79
86
  export { default as RichText } from './components/RichText.svelte';
80
87
  export type { RichTextProps } from './components/RichText.svelte';
81
88
  export type { RichTextDocument, RichTextLinkOptions } from './richtext/document.js';
package/dist/index.js CHANGED
@@ -3,6 +3,11 @@ export { default as IconButton } from './components/IconButton.svelte';
3
3
  export { default as Input } from './components/Input.svelte';
4
4
  export { default as Textarea } from './components/Textarea.svelte';
5
5
  export { default as Select } from './components/Select.svelte';
6
+ export { default as DatePicker } from './components/DatePicker.svelte';
7
+ export { default as TimePicker } from './components/TimePicker.svelte';
8
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
9
+ export { default as TimezonePicker } from './components/TimezonePicker.svelte';
10
+ export { default as ImageDropper } from './components/ImageDropper.svelte';
6
11
  export { default as SocialClientPicker } from './components/SocialClientPicker.svelte';
7
12
  export * from './social.js';
8
13
  export { default as Checkbox } from './components/Checkbox.svelte';
@@ -34,6 +39,7 @@ export { default as Tabs } from './components/Tabs.svelte';
34
39
  export { default as Icon } from './components/Icon.svelte';
35
40
  export { default as IconRail } from './components/IconRail.svelte';
36
41
  export { default as WorkspaceSidebar } from './components/WorkspaceSidebar.svelte';
42
+ export { default as AccountMenu } from './components/AccountMenu.svelte';
37
43
  export { default as LoginForm } from './components/login/LoginForm.svelte';
38
44
  export { default as LoginDialog } from './components/login/LoginDialog.svelte';
39
45
  export { default as SignupPermissions } from './components/login/SignupPermissions.svelte';
@@ -51,6 +57,7 @@ export { default as InlineEditor } from './components/InlineEditor.svelte';
51
57
  export { default as PostComposer } from './components/PostComposer.svelte';
52
58
  export { default as NestedComments } from './components/NestedComments.svelte';
53
59
  export { default as PostContent } from './components/PostContent.svelte';
60
+ export { safeHttpUrl, safeLinkHref } from './urls.js';
54
61
  export { default as RichText } from './components/RichText.svelte';
55
62
  export { default as ExternalEmbed } from './components/ExternalEmbed.svelte';
56
63
  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;
@@ -31,7 +31,7 @@ export function mentionSuggestions(search, host) {
31
31
  empty.role = 'option';
32
32
  empty.setAttribute('aria-disabled', 'true');
33
33
  empty.setAttribute('aria-selected', 'false');
34
- empty.className = 'p-3 text-xs text-muted';
34
+ empty.className = 'p-3 text-xs text-base-600 dark:text-base-400';
35
35
  empty.textContent = 'No accounts found';
36
36
  menu.append(empty);
37
37
  }
@@ -42,12 +42,12 @@ export function mentionSuggestions(search, host) {
42
42
  option.id = `${menu.id}-${index}`;
43
43
  option.setAttribute('aria-selected', String(index === selected));
44
44
  option.className =
45
- 'block w-full rounded-ui px-3 py-2 text-left text-xs text-ink hover:bg-recess aria-selected:bg-recess';
45
+ 'block w-full rounded-ui px-3 py-2 text-left text-xs text-base-950 dark:text-base-50 hover:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:hover:bg-base-950 aria-selected:bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:aria-selected:bg-base-950';
46
46
  const name = document.createElement('strong');
47
47
  name.className = 'block truncate font-medium';
48
48
  name.textContent = actor.displayName || actor.handle;
49
49
  const handle = document.createElement('span');
50
- handle.className = 'block truncate text-muted';
50
+ handle.className = 'block truncate text-base-600 dark:text-base-400';
51
51
  handle.textContent = `@${actor.handle}`;
52
52
  option.append(name, handle);
53
53
  option.onmousedown = (event) => event.preventDefault();
@@ -84,7 +84,7 @@ export function mentionSuggestions(search, host) {
84
84
  menu.role = 'listbox';
85
85
  menu.setAttribute('aria-label', 'Mention suggestions');
86
86
  menu.className =
87
- 'absolute z-60 w-70 max-w-full overflow-hidden rounded-ui border border-line bg-page p-1 font-sans text-ink';
87
+ 'absolute z-60 w-70 max-w-full overflow-hidden rounded-ui border border-base-300 dark:border-[color-mix(in_srgb,var(--base-800)_80%,var(--base-950))] bg-base-50 dark:bg-base-925 p-1 font-sans text-base-950 dark:text-base-50';
88
88
  host().append(menu);
89
89
  draw();
90
90
  },
@@ -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/styles.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const focus = "focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-focus";
2
- export declare const control = "block min-h-11 w-full min-w-0 rounded-ui border border-control-line bg-field px-3 py-2.5 text-base leading-normal text-ink shadow-(--ui-recess-shadow) placeholder:text-muted transition-colors duration-(--atmo-duration) motion-reduce:transition-none hover:border-control-hover focus:ring-0 focus:ring-offset-0 focus-visible:border-focus focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-focus disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:border-control-line aria-invalid:border-action aria-invalid:hover:border-action sm:text-sm";
1
+ export declare const focus = "focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-accent-700 dark:focus-visible:outline-accent-300";
2
+ export declare const control = "block min-h-11 w-full min-w-0 rounded-ui border border-base-300 dark:border-base-800 bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:bg-base-975 px-3 py-2.5 text-base leading-normal text-base-950 dark:text-base-50 shadow-[inset_0_2px_4px_color-mix(in_srgb,var(--base-950)_8%,transparent),inset_0_5px_12px_color-mix(in_srgb,var(--base-950)_2%,transparent)] dark:shadow-[inset_0_2px_4px_#0006,inset_0_5px_12px_#0002] placeholder:text-base-600 dark:placeholder:text-base-400 transition-colors duration-(--atmo-duration) motion-reduce:transition-none hover:border-base-400 dark:hover:border-base-700 focus:ring-0 focus:ring-offset-0 focus-visible:border-accent-700 dark:focus-visible:border-accent-300 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-accent-700 dark:focus-visible:outline-accent-300 disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:border-base-300 dark:disabled:hover:border-base-800 aria-invalid:border-accent-700 dark:aria-invalid:border-accent-300 aria-invalid:hover:border-accent-700 dark:aria-invalid:hover:border-accent-300 sm:text-sm";
package/dist/styles.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Shared Tailwind recipes, not an additional component stylesheet.
2
- export const focus = 'focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-focus';
3
- export const control = 'block min-h-11 w-full min-w-0 rounded-ui border border-control-line bg-field px-3 py-2.5 text-base leading-normal text-ink shadow-(--ui-recess-shadow) placeholder:text-muted transition-colors duration-(--atmo-duration) motion-reduce:transition-none hover:border-control-hover focus:ring-0 focus:ring-offset-0 focus-visible:border-focus focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-focus disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:border-control-line aria-invalid:border-action aria-invalid:hover:border-action sm:text-sm';
2
+ export const focus = 'focus-visible:outline-2 focus-visible:outline-offset-3 focus-visible:outline-accent-700 dark:focus-visible:outline-accent-300';
3
+ export const control = 'block min-h-11 w-full min-w-0 rounded-ui border border-base-300 dark:border-base-800 bg-[color-mix(in_srgb,var(--base-100)_75%,var(--base-200))] dark:bg-base-975 px-3 py-2.5 text-base leading-normal text-base-950 dark:text-base-50 shadow-[inset_0_2px_4px_color-mix(in_srgb,var(--base-950)_8%,transparent),inset_0_5px_12px_color-mix(in_srgb,var(--base-950)_2%,transparent)] dark:shadow-[inset_0_2px_4px_#0006,inset_0_5px_12px_#0002] placeholder:text-base-600 dark:placeholder:text-base-400 transition-colors duration-(--atmo-duration) motion-reduce:transition-none hover:border-base-400 dark:hover:border-base-700 focus:ring-0 focus:ring-offset-0 focus-visible:border-accent-700 dark:focus-visible:border-accent-300 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-accent-700 dark:focus-visible:outline-accent-300 disabled:cursor-not-allowed disabled:opacity-45 disabled:hover:border-base-300 dark:disabled:hover:border-base-800 aria-invalid:border-accent-700 dark:aria-invalid:border-accent-300 aria-invalid:hover:border-accent-700 dark:aria-invalid:hover:border-accent-300 sm:text-sm';
package/dist/theme.css CHANGED
@@ -4,6 +4,18 @@
4
4
  /* Resolve relative to this stylesheet in src/lib or the packaged dist directory. */
5
5
  @source './';
6
6
 
7
+ /* Explicit document modes win; without one, follow the browser preference. */
8
+ @custom-variant dark {
9
+ &:where(.dark, .dark *, [data-theme='dark'], [data-theme='dark'] *) {
10
+ @slot;
11
+ }
12
+ @media (prefers-color-scheme: dark) {
13
+ &:not(:where(.light, .light *, [data-theme='light'], [data-theme='light'] *)) {
14
+ @slot;
15
+ }
16
+ }
17
+ }
18
+
7
19
  @theme inline {
8
20
  --color-base-50: var(--base-50);
9
21
  --color-base-100: var(--base-100);
@@ -15,7 +27,9 @@
15
27
  --color-base-700: var(--base-700);
16
28
  --color-base-800: var(--base-800);
17
29
  --color-base-900: var(--base-900);
30
+ --color-base-925: var(--base-925);
18
31
  --color-base-950: var(--base-950);
32
+ --color-base-975: var(--base-975);
19
33
  --color-accent-50: var(--accent-50);
20
34
  --color-accent-100: var(--accent-100);
21
35
  --color-accent-200: var(--accent-200);
@@ -26,19 +40,9 @@
26
40
  --color-accent-700: var(--accent-700);
27
41
  --color-accent-800: var(--accent-800);
28
42
  --color-accent-900: var(--accent-900);
43
+ --color-accent-925: var(--accent-925);
29
44
  --color-accent-950: var(--accent-950);
30
- --color-page: var(--ui-page);
31
- --color-recess: var(--ui-recess);
32
- --color-surface: var(--ui-surface);
33
- --color-ink: var(--ui-ink);
34
- --color-muted: var(--ui-muted);
35
- --color-line: var(--ui-line);
36
- --color-control-line: var(--ui-control-line);
37
- --color-control-hover: var(--ui-control-hover);
38
- --color-field: var(--ui-field);
39
- --color-action: var(--ui-action);
40
- --color-accent-soft: var(--ui-accent-soft);
41
- --color-focus: var(--ui-focus);
45
+ --color-accent-975: var(--accent-975);
42
46
  --font-sans: var(--atmo-font-sans);
43
47
  --radius-ui: var(--atmo-radius-control);
44
48
  --radius-panel: var(--atmo-radius-panel);
@@ -82,9 +86,8 @@
82
86
  color-scheme: light;
83
87
  }
84
88
 
85
- /* Recompute semantic tokens at palette boundaries, not only at :root.
86
- A red button inside a blue section must resolve its own complete palette.
87
- Add atmo-theme to a custom-ramp boundary to get the same behavior. */
89
+ /* Derive the extra shades at palette boundaries, including custom ramps
90
+ marked with atmo-theme. Ratios preserve the existing page/field colors. */
88
91
  :where(
89
92
  :root,
90
93
  .atmo,
@@ -118,38 +121,14 @@
118
121
  .pink,
119
122
  .rose
120
123
  ) {
121
- --ui-page: light-dark(var(--base-50), color-mix(in srgb, var(--base-900) 50%, var(--base-950)));
122
- --ui-recess: light-dark(
123
- color-mix(in srgb, var(--base-100) 75%, var(--base-200)),
124
- var(--base-950)
125
- );
126
- --ui-surface: light-dark(var(--base-50), var(--base-900));
127
- --ui-ink: light-dark(var(--base-950), var(--base-50));
128
- --ui-muted: light-dark(var(--base-600), var(--base-400));
129
- --ui-line: light-dark(
130
- var(--base-300),
131
- color-mix(in srgb, var(--base-800) 80%, var(--base-950))
132
- );
133
- --ui-field: light-dark(
134
- color-mix(in srgb, var(--base-100) 75%, var(--base-200)),
135
- color-mix(in srgb, var(--base-950) 80%, black)
136
- );
137
- --ui-control-line: light-dark(var(--base-300), var(--base-800));
138
- --ui-control-hover: light-dark(var(--base-400), var(--base-700));
139
- --ui-action: light-dark(var(--accent-700), var(--accent-300));
140
- --ui-accent-soft: light-dark(
141
- var(--accent-50),
142
- color-mix(in srgb, var(--accent-950) 65%, var(--base-950))
143
- );
144
- --ui-focus: light-dark(var(--accent-700), var(--accent-300));
145
- --ui-shadow: light-dark(color-mix(in srgb, var(--base-950) 8%, transparent), #0006);
146
- --ui-shadow-soft: light-dark(color-mix(in srgb, var(--base-950) 2%, transparent), #0002);
147
- --ui-recess-shadow: inset 0 2px 4px var(--ui-shadow), inset 0 5px 12px var(--ui-shadow-soft);
124
+ --base-925: color-mix(in srgb, var(--base-900) 50%, var(--base-950));
125
+ --base-975: color-mix(in srgb, var(--base-950) 80%, black);
126
+ --accent-925: color-mix(in srgb, var(--accent-900) 50%, var(--accent-950));
127
+ --accent-975: color-mix(in srgb, var(--accent-950) 80%, black);
148
128
  }
149
129
  .atmo {
150
130
  font-family: var(--atmo-font-sans);
151
- color: var(--ui-ink);
152
- background: var(--ui-page);
131
+ @apply bg-base-50 text-base-950 dark:bg-base-925 dark:text-base-50;
153
132
  -webkit-font-smoothing: antialiased;
154
133
  }
155
134
  .gray {
package/dist/theme.js CHANGED
@@ -1,10 +1,9 @@
1
- // Portals leave their original DOM ancestry. Carry the originating palette,
2
- // semantic tokens, font, and color scheme with them; behavior stays in Bits UI.
1
+ // Portals inherit the document's light/dark mode normally. Only carry scoped
2
+ // palettes and typography, which would otherwise be lost outside their ancestry.
3
3
  export function readTheme(element) {
4
4
  const style = getComputedStyle(element);
5
- const variables = Array.from(style).filter((name) => ['--base-', '--accent-', '--ui-', '--atmo-'].some((prefix) => name.startsWith(prefix)));
5
+ const variables = Array.from(style).filter((name) => ['--base-', '--accent-', '--atmo-'].some((prefix) => name.startsWith(prefix)));
6
6
  return [
7
- `color-scheme:${style.colorScheme}`,
8
7
  `font-family:${style.fontFamily}`,
9
8
  ...variables.map((name) => `${name}:${style.getPropertyValue(name)}`)
10
9
  ].join(';');
@@ -19,12 +18,5 @@ export function observeTheme(element, update) {
19
18
  });
20
19
  parent = parent.parentElement;
21
20
  }
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
- };
21
+ return () => observer.disconnect();
30
22
  }
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.10.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"
File without changes