@abraca/nuxt 2.0.0 → 2.0.3

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 (135) hide show
  1. package/dist/module.d.mts +18 -7
  2. package/dist/module.json +1 -1
  3. package/dist/module.mjs +21 -5
  4. package/dist/runtime/assets/aware-tokens.css +1 -0
  5. package/dist/runtime/components/AAccountSwitcherModal.d.vue.ts +16 -1
  6. package/dist/runtime/components/AAccountSwitcherModal.vue +33 -4
  7. package/dist/runtime/components/AAccountSwitcherModal.vue.d.ts +16 -1
  8. package/dist/runtime/components/AAuthLinkLanding.d.vue.ts +3 -0
  9. package/dist/runtime/components/AAuthLinkLanding.vue +85 -0
  10. package/dist/runtime/components/AAuthLinkLanding.vue.d.ts +3 -0
  11. package/dist/runtime/components/AClaimAccountModal.d.vue.ts +7 -1
  12. package/dist/runtime/components/AClaimAccountModal.vue +28 -13
  13. package/dist/runtime/components/AClaimAccountModal.vue.d.ts +7 -1
  14. package/dist/runtime/components/AEditor.vue +5 -0
  15. package/dist/runtime/components/AEmailVerifyConfirmModal.d.vue.ts +30 -0
  16. package/dist/runtime/components/AEmailVerifyConfirmModal.vue +100 -0
  17. package/dist/runtime/components/AEmailVerifyConfirmModal.vue.d.ts +30 -0
  18. package/dist/runtime/components/AEmailVerifyRequestCard.d.vue.ts +22 -0
  19. package/dist/runtime/components/AEmailVerifyRequestCard.vue +65 -0
  20. package/dist/runtime/components/AEmailVerifyRequestCard.vue.d.ts +22 -0
  21. package/dist/runtime/components/AMnemonicLoginModal.d.vue.ts +1 -1
  22. package/dist/runtime/components/AMnemonicLoginModal.vue.d.ts +1 -1
  23. package/dist/runtime/components/ANodePanel.vue +2 -0
  24. package/dist/runtime/components/ANotificationBell.d.vue.ts +2 -2
  25. package/dist/runtime/components/ANotificationBell.vue.d.ts +2 -2
  26. package/dist/runtime/components/APasswordChangeModal.d.vue.ts +28 -0
  27. package/dist/runtime/components/APasswordChangeModal.vue +178 -0
  28. package/dist/runtime/components/APasswordChangeModal.vue.d.ts +28 -0
  29. package/dist/runtime/components/APasswordLoginModal.d.vue.ts +42 -0
  30. package/dist/runtime/components/APasswordLoginModal.vue +177 -0
  31. package/dist/runtime/components/APasswordLoginModal.vue.d.ts +42 -0
  32. package/dist/runtime/components/APasswordRegisterModal.d.vue.ts +49 -0
  33. package/dist/runtime/components/APasswordRegisterModal.vue +262 -0
  34. package/dist/runtime/components/APasswordRegisterModal.vue.d.ts +49 -0
  35. package/dist/runtime/components/APasswordResetConfirmModal.d.vue.ts +31 -0
  36. package/dist/runtime/components/APasswordResetConfirmModal.vue +154 -0
  37. package/dist/runtime/components/APasswordResetConfirmModal.vue.d.ts +31 -0
  38. package/dist/runtime/components/APasswordResetRequestModal.d.vue.ts +35 -0
  39. package/dist/runtime/components/APasswordResetRequestModal.vue +113 -0
  40. package/dist/runtime/components/APasswordResetRequestModal.vue.d.ts +35 -0
  41. package/dist/runtime/components/ASetPasswordCard.d.vue.ts +26 -0
  42. package/dist/runtime/components/ASetPasswordCard.vue +139 -0
  43. package/dist/runtime/components/ASetPasswordCard.vue.d.ts +26 -0
  44. package/dist/runtime/components/ASubPageList.d.vue.ts +66 -0
  45. package/dist/runtime/components/ASubPageList.vue +147 -0
  46. package/dist/runtime/components/ASubPageList.vue.d.ts +66 -0
  47. package/dist/runtime/components/aware/AAccordion.d.vue.ts +2 -0
  48. package/dist/runtime/components/aware/AAccordion.vue +11 -1
  49. package/dist/runtime/components/aware/AAccordion.vue.d.ts +2 -0
  50. package/dist/runtime/components/aware/AButton.vue +3 -3
  51. package/dist/runtime/components/aware/ACollapsible.d.vue.ts +2 -0
  52. package/dist/runtime/components/aware/ACollapsible.vue +9 -1
  53. package/dist/runtime/components/aware/ACollapsible.vue.d.ts +2 -0
  54. package/dist/runtime/components/aware/AGlobalFocusLayer.vue +1 -1
  55. package/dist/runtime/components/aware/AHoverItem.vue +28 -3
  56. package/dist/runtime/components/aware/AMedia.d.vue.ts +1 -1
  57. package/dist/runtime/components/aware/AMedia.vue.d.ts +1 -1
  58. package/dist/runtime/components/aware/AModal.d.vue.ts +2 -0
  59. package/dist/runtime/components/aware/AModal.vue +9 -1
  60. package/dist/runtime/components/aware/AModal.vue.d.ts +2 -0
  61. package/dist/runtime/components/aware/APresenceBlobs.vue +1 -1
  62. package/dist/runtime/components/aware/APresenceCursors.vue +1 -1
  63. package/dist/runtime/components/aware/AScroll.d.vue.ts +2 -0
  64. package/dist/runtime/components/aware/AScroll.vue +13 -3
  65. package/dist/runtime/components/aware/AScroll.vue.d.ts +2 -0
  66. package/dist/runtime/components/aware/ASlideover.d.vue.ts +2 -0
  67. package/dist/runtime/components/aware/ASlideover.vue +9 -1
  68. package/dist/runtime/components/aware/ASlideover.vue.d.ts +2 -0
  69. package/dist/runtime/components/aware/ASlider.vue +1 -0
  70. package/dist/runtime/components/aware/ATabs.d.vue.ts +2 -0
  71. package/dist/runtime/components/aware/ATabs.vue +9 -1
  72. package/dist/runtime/components/aware/ATabs.vue.d.ts +2 -0
  73. package/dist/runtime/components/chat/ANodeChatPanel.vue +1 -0
  74. package/dist/runtime/components/editor/AEditorRedoButton.d.vue.ts +2 -2
  75. package/dist/runtime/components/editor/AEditorRedoButton.vue.d.ts +2 -2
  76. package/dist/runtime/components/editor/AEditorUndoButton.d.vue.ts +2 -2
  77. package/dist/runtime/components/editor/AEditorUndoButton.vue.d.ts +2 -2
  78. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.d.vue.ts +4 -4
  79. package/dist/runtime/components/renderers/calendar/ACalendarToolbar.vue.d.ts +4 -4
  80. package/dist/runtime/components/renderers/media/MediaTransportBar.d.vue.ts +2 -2
  81. package/dist/runtime/components/renderers/media/MediaTransportBar.vue.d.ts +2 -2
  82. package/dist/runtime/components/shell/AUserProfilePopover.d.vue.ts +1 -1
  83. package/dist/runtime/components/shell/AUserProfilePopover.vue.d.ts +1 -1
  84. package/dist/runtime/composables/useAAField.js +7 -4
  85. package/dist/runtime/composables/useAAFocus.js +10 -5
  86. package/dist/runtime/composables/useAAFollowAnchor.js +68 -34
  87. package/dist/runtime/composables/useAAFollowPeer.d.ts +7 -4
  88. package/dist/runtime/composables/useAAFollowPeer.js +60 -11
  89. package/dist/runtime/composables/useAAViewport.d.ts +1 -1
  90. package/dist/runtime/composables/useAbracadabraAuth.d.ts +2 -0
  91. package/dist/runtime/composables/useAbracadabraAuth.js +2 -0
  92. package/dist/runtime/composables/useEditorSuggestions.js +2 -1
  93. package/dist/runtime/composables/useEmailVerification.d.ts +40 -26
  94. package/dist/runtime/composables/useEmailVerification.js +95 -43
  95. package/dist/runtime/composables/usePasswordAuth.d.ts +64 -0
  96. package/dist/runtime/composables/usePasswordAuth.js +126 -0
  97. package/dist/runtime/composables/useTiptapHistory.d.ts +2 -2
  98. package/dist/runtime/composables/useTiptapHistory.js +5 -5
  99. package/dist/runtime/extensions/svg-embed.d.ts +23 -0
  100. package/dist/runtime/extensions/svg-embed.js +33 -0
  101. package/dist/runtime/extensions/views/MetaFieldView.vue +23 -6
  102. package/dist/runtime/extensions/views/SvgEmbedView.d.vue.ts +4 -0
  103. package/dist/runtime/extensions/views/SvgEmbedView.vue +120 -0
  104. package/dist/runtime/extensions/views/SvgEmbedView.vue.d.ts +4 -0
  105. package/dist/runtime/plugin-abracadabra.client.js +58 -9
  106. package/dist/runtime/plugin-abracadabra.server.js +2 -0
  107. package/dist/runtime/plugins/core.plugin.js +8 -4
  108. package/dist/runtime/server/plugins/abracadabra-service.js +102 -13
  109. package/dist/runtime/types.d.ts +11 -0
  110. package/dist/runtime/utils/awareRingStyle.js +1 -1
  111. package/dist/runtime/utils/sanitizeSvg.d.ts +19 -0
  112. package/dist/runtime/utils/sanitizeSvg.js +87 -0
  113. package/package.json +7 -8
  114. package/dist/runtime/components/renderers/ASpatialRenderer.d.vue.ts +0 -19
  115. package/dist/runtime/components/renderers/ASpatialRenderer.vue +0 -459
  116. package/dist/runtime/components/renderers/ASpatialRenderer.vue.d.ts +0 -19
  117. package/dist/runtime/components/renderers/spatial/SpatialGround.d.vue.ts +0 -20
  118. package/dist/runtime/components/renderers/spatial/SpatialGround.vue +0 -26
  119. package/dist/runtime/components/renderers/spatial/SpatialGround.vue.d.ts +0 -20
  120. package/dist/runtime/components/renderers/spatial/SpatialObject.d.vue.ts +0 -17
  121. package/dist/runtime/components/renderers/spatial/SpatialObject.vue +0 -257
  122. package/dist/runtime/components/renderers/spatial/SpatialObject.vue.d.ts +0 -17
  123. package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.d.vue.ts +0 -15
  124. package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.vue +0 -18
  125. package/dist/runtime/components/renderers/spatial/SpatialSceneBridge.vue.d.ts +0 -15
  126. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.d.vue.ts +0 -16
  127. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue +0 -66
  128. package/dist/runtime/components/renderers/spatial/SpatialTransformInputs.vue.d.ts +0 -16
  129. package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.d.vue.ts +0 -8
  130. package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.vue +0 -53
  131. package/dist/runtime/components/renderers/spatial/SpatialUserAvatar.vue.d.ts +0 -8
  132. package/dist/runtime/composables/useSpatialCamera.d.ts +0 -16
  133. package/dist/runtime/composables/useSpatialCamera.js +0 -175
  134. package/dist/runtime/composables/useSpatialDrag.d.ts +0 -14
  135. package/dist/runtime/composables/useSpatialDrag.js +0 -137
@@ -1,58 +1,110 @@
1
1
  import { ref } from "vue";
2
- import { useAbracadabra } from "#imports";
2
+ import { useAbracadabra } from "./useAbracadabra.js";
3
3
  const verified = ref(false);
4
- const isRequesting = ref(false);
5
- const isConfirming = ref(false);
6
- const error = ref(null);
7
4
  const lastSentAt = ref(null);
8
- async function request() {
9
- const abra = useAbracadabra();
10
- const client = abra.client.value;
11
- if (!client) throw new Error("Not connected");
12
- isRequesting.value = true;
13
- error.value = null;
14
- try {
15
- await client.requestEmailVerification();
16
- lastSentAt.value = Date.now();
17
- } catch (e) {
18
- error.value = e instanceof Error ? e.message : "Failed to send verification email";
19
- throw e;
20
- } finally {
21
- isRequesting.value = false;
22
- }
5
+ const busy = ref(null);
6
+ const error = ref(null);
7
+ const lastSuccess = ref(null);
8
+ function extractServerMessage(raw) {
9
+ const trailing = raw.replace(/ \(\d+\)$/, "").trim();
10
+ const colon = trailing.indexOf(": ");
11
+ if (colon === -1) return trailing;
12
+ const head = trailing.slice(0, colon);
13
+ if (/^[A-Z]+ \//.test(head)) return trailing.slice(colon + 2).trim();
14
+ return trailing;
23
15
  }
24
- async function confirm(token) {
25
- const abra = useAbracadabra();
26
- const client = abra.client.value;
27
- if (!client) throw new Error("Not connected");
28
- isConfirming.value = true;
29
- error.value = null;
30
- try {
31
- await client.confirmEmailVerification(token);
32
- verified.value = true;
33
- } catch (e) {
34
- error.value = e instanceof Error ? e.message : "Verification failed";
35
- verified.value = false;
36
- throw e;
37
- } finally {
38
- isConfirming.value = false;
16
+ function classifyError(err) {
17
+ if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
18
+ return { code: "network_error", message: err.message };
19
+ }
20
+ if (!(err instanceof Error)) {
21
+ return { code: "unknown", message: String(err) };
22
+ }
23
+ const status = err.status;
24
+ const serverMsg = extractServerMessage(err.message);
25
+ const lower = serverMsg.toLowerCase();
26
+ const retryMatch = lower.match(/retry(?:[-\s]?after)?[:\s]+(\d+)/);
27
+ const retryAfterSeconds = retryMatch ? Number(retryMatch[1]) : void 0;
28
+ if (status === 429) {
29
+ return { code: "rate_limited", message: serverMsg, status, retryAfterSeconds };
30
+ }
31
+ if (status === 410 || /token (?:expired|invalid)/.test(lower)) {
32
+ return {
33
+ code: lower.includes("expired") ? "token_expired" : "token_invalid",
34
+ message: serverMsg,
35
+ status
36
+ };
37
+ }
38
+ if (status === 404 || /verification (?:is )?disabled/.test(lower)) {
39
+ return { code: "verification_disabled", message: serverMsg, status };
40
+ }
41
+ if (lower.includes("already verified") || lower.includes("already_verified")) {
42
+ return { code: "already_verified", message: serverMsg, status };
39
43
  }
44
+ if (lower.includes("no email") || lower.includes("email not set") || lower.includes("email_not_set")) {
45
+ return { code: "email_not_set", message: serverMsg, status };
46
+ }
47
+ if (status === 401) return { code: "unauthorized", message: serverMsg, status };
48
+ if (status === 403) return { code: "forbidden", message: serverMsg, status };
49
+ if (typeof status === "number" && status >= 500) {
50
+ return { code: "server_error", message: serverMsg, status };
51
+ }
52
+ return { code: "unknown", message: serverMsg, status };
40
53
  }
41
54
  export function useEmailVerification() {
55
+ const abra = useAbracadabra();
56
+ function clearError() {
57
+ error.value = null;
58
+ }
59
+ async function run(action, fn) {
60
+ const client = abra.client.value;
61
+ if (!client) {
62
+ const err = { code: "network_error", message: "Not connected" };
63
+ error.value = err;
64
+ return { ok: false, error: err };
65
+ }
66
+ busy.value = action;
67
+ error.value = null;
68
+ try {
69
+ await fn(client);
70
+ lastSuccess.value = action;
71
+ return { ok: true };
72
+ } catch (e) {
73
+ const err = classifyError(e);
74
+ error.value = err;
75
+ return { ok: false, error: err };
76
+ } finally {
77
+ busy.value = null;
78
+ }
79
+ }
80
+ async function request() {
81
+ const res = await run("request", (client) => client.requestEmailVerification());
82
+ if (res.ok) lastSentAt.value = Date.now();
83
+ return res;
84
+ }
85
+ async function confirm(opts) {
86
+ const res = await run("confirm", (client) => client.confirmEmailVerification(opts.token));
87
+ if (res.ok) verified.value = true;
88
+ return res;
89
+ }
42
90
  return {
43
- /** True after a successful `confirm()` in the current session. */
91
+ /** True after a successful `confirm()` in this session. */
44
92
  verified,
45
- /** Whether a `request()` is in flight. */
46
- isRequesting,
47
- /** Whether a `confirm()` is in flight. */
48
- isConfirming,
49
- /** Last error message, or null. */
50
- error,
51
93
  /** Epoch ms of the last successful `request()`, or null. */
52
94
  lastSentAt,
53
- /** Send a verification email to the current user. Requires auth. */
95
+ /** Currently-running action, or null. */
96
+ busy,
97
+ /** Most recent typed error, or null. Cleared at the start of every action. */
98
+ error,
99
+ /** Most recent successful action, or null. */
100
+ lastSuccess,
101
+ /** Wipe `error` without re-running anything. */
102
+ clearError,
103
+ /** Send a verification email to the current user. Auth required. */
54
104
  request,
55
105
  /** Confirm a verification token from the email link. No auth required. */
56
- confirm
106
+ confirm,
107
+ /** Internal helper exposed for testing. */
108
+ _classifyError: classifyError
57
109
  };
58
110
  }
@@ -0,0 +1,64 @@
1
+ /** Known error codes raised by `usePasswordAuth()` actions. */
2
+ export type PasswordAuthErrorCode = 'rate_limited' | 'locked_out' | 'password_too_short' | 'invalid_credentials' | 'token_expired' | 'token_invalid' | 'password_already_set' | 'email_taken' | 'username_taken' | 'password_login_disabled' | 'password_reset_disabled' | 'invite_required' | 'invite_invalid' | 'network_error' | 'forbidden' | 'unauthorized' | 'server_error' | 'unknown';
3
+ export interface PasswordAuthError {
4
+ code: PasswordAuthErrorCode;
5
+ message: string;
6
+ /** HTTP status code, when the error came from the REST API. */
7
+ status?: number;
8
+ /** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
9
+ retryAfterSeconds?: number;
10
+ }
11
+ export type PasswordAuthAction = 'login' | 'register' | 'reset-request' | 'reset-confirm' | 'change' | 'set';
12
+ export interface PasswordAuthResult {
13
+ ok: boolean;
14
+ error?: PasswordAuthError;
15
+ }
16
+ declare function classifyError(err: unknown): PasswordAuthError;
17
+ export declare function usePasswordAuth(): {
18
+ busy: import("vue").Ref<PasswordAuthAction | null, PasswordAuthAction | null>;
19
+ error: import("vue").Ref<{
20
+ code: PasswordAuthErrorCode;
21
+ message: string;
22
+ status?: number
23
+ /** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
24
+ | undefined;
25
+ retryAfterSeconds?: number | undefined;
26
+ } | null, PasswordAuthError | {
27
+ code: PasswordAuthErrorCode;
28
+ message: string;
29
+ status?: number
30
+ /** Seconds the caller should wait before retrying — only set for `rate_limited` / `locked_out`. */
31
+ | undefined;
32
+ retryAfterSeconds?: number | undefined;
33
+ } | null>;
34
+ lastSuccess: import("vue").Ref<PasswordAuthAction | null, PasswordAuthAction | null>;
35
+ clearError: () => void;
36
+ login: (opts: {
37
+ username: string;
38
+ password: string;
39
+ }) => Promise<PasswordAuthResult>;
40
+ register: (opts: {
41
+ username: string;
42
+ password: string;
43
+ email?: string;
44
+ displayName?: string;
45
+ inviteCode?: string;
46
+ }) => Promise<PasswordAuthResult>;
47
+ requestReset: (opts: {
48
+ identifier: string;
49
+ }) => Promise<PasswordAuthResult>;
50
+ confirmReset: (opts: {
51
+ token: string;
52
+ newPassword: string;
53
+ }) => Promise<PasswordAuthResult>;
54
+ changePassword: (opts: {
55
+ currentPassword: string;
56
+ newPassword: string;
57
+ }) => Promise<PasswordAuthResult>;
58
+ setPassword: (opts: {
59
+ password: string;
60
+ }) => Promise<PasswordAuthResult>;
61
+ /** Internal helper exposed for testing — classify any error to its code. */
62
+ _classifyError: typeof classifyError;
63
+ };
64
+ export {};
@@ -0,0 +1,126 @@
1
+ import { ref } from "vue";
2
+ import { useAbracadabraAuth } from "./useAbracadabraAuth.js";
3
+ function extractServerMessage(raw) {
4
+ const trailing = raw.replace(/ \(\d+\)$/, "").trim();
5
+ const colon = trailing.indexOf(": ");
6
+ if (colon === -1) return trailing;
7
+ const head = trailing.slice(0, colon);
8
+ if (/^[A-Z]+ \//.test(head)) return trailing.slice(colon + 2).trim();
9
+ return trailing;
10
+ }
11
+ function classifyError(err) {
12
+ if (err instanceof TypeError && /fetch|network/i.test(err.message)) {
13
+ return { code: "network_error", message: err.message };
14
+ }
15
+ if (!(err instanceof Error)) {
16
+ return { code: "unknown", message: String(err) };
17
+ }
18
+ const status = err.status;
19
+ const serverMsg = extractServerMessage(err.message);
20
+ const lower = serverMsg.toLowerCase();
21
+ const retryMatch = lower.match(/retry(?:[-\s]?after)?[:\s]+(\d+)/);
22
+ const retryAfterSeconds = retryMatch ? Number(retryMatch[1]) : void 0;
23
+ if (status === 423) {
24
+ return { code: "locked_out", message: serverMsg, status, retryAfterSeconds };
25
+ }
26
+ if (status === 429) {
27
+ return { code: "rate_limited", message: serverMsg, status, retryAfterSeconds };
28
+ }
29
+ if (status === 410 || /token (?:expired|invalid)/.test(lower)) {
30
+ return {
31
+ code: lower.includes("expired") ? "token_expired" : "token_invalid",
32
+ message: serverMsg,
33
+ status
34
+ };
35
+ }
36
+ if (lower.includes("password too short")) {
37
+ return { code: "password_too_short", message: serverMsg, status };
38
+ }
39
+ if (lower.includes("password_already_set")) {
40
+ return { code: "password_already_set", message: serverMsg, status };
41
+ }
42
+ if (lower.includes("password login is disabled")) {
43
+ return { code: "password_login_disabled", message: serverMsg, status };
44
+ }
45
+ if (lower.includes("password reset disabled") || lower.includes("password reset is disabled")) {
46
+ return { code: "password_reset_disabled", message: serverMsg, status };
47
+ }
48
+ if (lower.includes("username") && (lower.includes("taken") || lower.includes("exists") || lower.includes("in use"))) {
49
+ return { code: "username_taken", message: serverMsg, status };
50
+ }
51
+ if (lower.includes("email") && (lower.includes("taken") || lower.includes("exists") || lower.includes("in use"))) {
52
+ return { code: "email_taken", message: serverMsg, status };
53
+ }
54
+ if (lower.includes("invite required") || lower.includes("invite is required")) {
55
+ return { code: "invite_required", message: serverMsg, status };
56
+ }
57
+ if (lower.includes("invite") && (lower.includes("invalid") || lower.includes("expired") || lower.includes("used"))) {
58
+ return { code: "invite_invalid", message: serverMsg, status };
59
+ }
60
+ if (status === 401 || lower.includes("unauthorized") || lower.includes("invalid credentials") || lower.includes("bad password")) {
61
+ return { code: status === 401 ? "unauthorized" : "invalid_credentials", message: serverMsg, status };
62
+ }
63
+ if (status === 403) {
64
+ return { code: "forbidden", message: serverMsg, status };
65
+ }
66
+ if (typeof status === "number" && status >= 500) {
67
+ return { code: "server_error", message: serverMsg, status };
68
+ }
69
+ return { code: "unknown", message: serverMsg, status };
70
+ }
71
+ export function usePasswordAuth() {
72
+ const auth = useAbracadabraAuth();
73
+ const busy = ref(null);
74
+ const error = ref(null);
75
+ const lastSuccess = ref(null);
76
+ function clearError() {
77
+ error.value = null;
78
+ }
79
+ async function run(action, fn) {
80
+ busy.value = action;
81
+ error.value = null;
82
+ try {
83
+ await fn();
84
+ lastSuccess.value = action;
85
+ return { ok: true };
86
+ } catch (e) {
87
+ const err = classifyError(e);
88
+ error.value = err;
89
+ return { ok: false, error: err };
90
+ } finally {
91
+ busy.value = null;
92
+ }
93
+ }
94
+ async function login(opts) {
95
+ return run("login", () => auth.loginWithPassword(opts));
96
+ }
97
+ async function register(opts) {
98
+ return run("register", () => auth.registerWithPassword(opts));
99
+ }
100
+ async function requestReset(opts) {
101
+ return run("reset-request", () => auth.requestPasswordReset(opts));
102
+ }
103
+ async function confirmReset(opts) {
104
+ return run("reset-confirm", () => auth.confirmPasswordReset(opts));
105
+ }
106
+ async function changePassword(opts) {
107
+ return run("change", () => auth.changePassword(opts));
108
+ }
109
+ async function setPassword(opts) {
110
+ return run("set", () => auth.setPassword(opts.password));
111
+ }
112
+ return {
113
+ busy,
114
+ error,
115
+ lastSuccess,
116
+ clearError,
117
+ login,
118
+ register,
119
+ requestReset,
120
+ confirmReset,
121
+ changePassword,
122
+ setPassword,
123
+ /** Internal helper exposed for testing — classify any error to its code. */
124
+ _classifyError: classifyError
125
+ };
126
+ }
@@ -12,9 +12,9 @@
12
12
  * const history = useTiptapHistory(editorRef)
13
13
  * <UButton :disabled="!history.canUndo.value" @click="history.undo" />
14
14
  */
15
- import { type MaybeRef } from 'vue';
15
+ import { type MaybeRefOrGetter } from 'vue';
16
16
  import type { Editor } from '@tiptap/vue-3';
17
- export declare function useTiptapHistory(editor: MaybeRef<Editor | null | undefined>): {
17
+ export declare function useTiptapHistory(editor: MaybeRefOrGetter<Editor | null | undefined>): {
18
18
  canUndo: import("vue").ComputedRef<boolean>;
19
19
  canRedo: import("vue").ComputedRef<boolean>;
20
20
  undo: () => void;
@@ -1,12 +1,12 @@
1
- import { computed, unref } from "vue";
1
+ import { computed, toValue } from "vue";
2
2
  export function useTiptapHistory(editor) {
3
- const canUndo = computed(() => !!unref(editor)?.can?.().undo?.());
4
- const canRedo = computed(() => !!unref(editor)?.can?.().redo?.());
3
+ const canUndo = computed(() => !!toValue(editor)?.can?.().undo?.());
4
+ const canRedo = computed(() => !!toValue(editor)?.can?.().redo?.());
5
5
  function undo() {
6
- unref(editor)?.commands?.undo?.();
6
+ toValue(editor)?.commands?.undo?.();
7
7
  }
8
8
  function redo() {
9
- unref(editor)?.commands?.redo?.();
9
+ toValue(editor)?.commands?.redo?.();
10
10
  }
11
11
  return { canUndo, canRedo, undo, redo };
12
12
  }
@@ -0,0 +1,23 @@
1
+ import { Node } from '@tiptap/core';
2
+ declare module '@tiptap/core' {
3
+ interface Commands<ReturnType> {
4
+ svgEmbed: {
5
+ insertSvgEmbed: (attrs: {
6
+ svg?: string;
7
+ title?: string;
8
+ width?: string | null;
9
+ height?: string | null;
10
+ }) => ReturnType;
11
+ };
12
+ }
13
+ }
14
+ /**
15
+ * SVG embed extension.
16
+ *
17
+ * The extension itself has no peer dep. Sanitization happens inside the
18
+ * NodeView via `runtime/utils/sanitizeSvg.ts`, which try-imports DOMPurify
19
+ * and falls back to a strict built-in allowlist when DOMPurify isn't
20
+ * installed. Apps that need richer SVG features (animations, CSS) should
21
+ * `pnpm add dompurify`.
22
+ */
23
+ export declare const SvgEmbed: Node<any, any>;
@@ -0,0 +1,33 @@
1
+ import { Node, mergeAttributes } from "@tiptap/core";
2
+ import { VueNodeViewRenderer } from "@tiptap/vue-3";
3
+ import SvgEmbedView from "./views/SvgEmbedView.vue";
4
+ export const SvgEmbed = Node.create({
5
+ name: "svgEmbed",
6
+ group: "block",
7
+ atom: true,
8
+ draggable: true,
9
+ addAttributes() {
10
+ return {
11
+ svg: { default: "" },
12
+ title: { default: "" },
13
+ width: { default: null },
14
+ height: { default: null }
15
+ };
16
+ },
17
+ parseHTML() {
18
+ return [{ tag: 'div[data-type="svg-embed"]' }];
19
+ },
20
+ renderHTML({ HTMLAttributes }) {
21
+ return ["div", mergeAttributes(HTMLAttributes, { "data-type": "svg-embed" })];
22
+ },
23
+ addNodeView() {
24
+ return VueNodeViewRenderer(SvgEmbedView);
25
+ },
26
+ addCommands() {
27
+ return {
28
+ insertSvgEmbed: (attrs) => ({ commands }) => {
29
+ return commands.insertContent({ type: this.name, attrs });
30
+ }
31
+ };
32
+ }
33
+ });
@@ -140,12 +140,23 @@ function onDateTimeRange(range) {
140
140
  [endKey.value]: `${dvToIso(range.end)}T${et}:00.000Z`
141
141
  });
142
142
  }
143
- function onStartTime(t2) {
143
+ function asSingleTime(t2) {
144
+ if (t2 && typeof t2 === "object" && "hour" in t2 && "minute" in t2) {
145
+ const v = t2;
146
+ if (typeof v.hour === "number" && typeof v.minute === "number") {
147
+ return { hour: v.hour, minute: v.minute };
148
+ }
149
+ }
150
+ return null;
151
+ }
152
+ function onStartTime(raw) {
153
+ const t2 = asSingleTime(raw);
144
154
  if (!t2) return;
145
155
  const date = getStr(startKey.value).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
146
156
  patch({ [startKey.value]: `${date}T${timeToHHMM(t2)}:00.000Z` });
147
157
  }
148
- function onEndTime(t2) {
158
+ function onEndTime(raw) {
159
+ const t2 = asSingleTime(raw);
149
160
  if (!t2) return;
150
161
  const date = getStr(endKey.value).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
151
162
  patch({ [endKey.value]: `${date}T${timeToHHMM(t2)}:00.000Z` });
@@ -257,15 +268,18 @@ function onSingleDatetimeDate(d) {
257
268
  const time = getStr(metaKey.value).slice(11, 16) || "00:00";
258
269
  patch({ [metaKey.value]: `${date}T${time}:00.000Z` });
259
270
  }
260
- function onSingleTime(t2) {
271
+ function onSingleTime(raw) {
272
+ const t2 = asSingleTime(raw);
261
273
  if (!t2) return;
262
274
  patch({ [metaKey.value]: timeToHHMM(t2) });
263
275
  }
264
- function onTimeRangeStart(t2) {
276
+ function onTimeRangeStart(raw) {
277
+ const t2 = asSingleTime(raw);
265
278
  if (!t2) return;
266
279
  patch({ [startKey.value]: timeToHHMM(t2) });
267
280
  }
268
- function onTimeRangeEnd(t2) {
281
+ function onTimeRangeEnd(raw) {
282
+ const t2 = asSingleTime(raw);
269
283
  if (!t2) return;
270
284
  patch({ [endKey.value]: timeToHHMM(t2) });
271
285
  }
@@ -1089,7 +1103,10 @@ function removeOption(opt) {
1089
1103
  <UInputTime
1090
1104
  size="sm"
1091
1105
  :model-value="isoToTime(getStr(metaKey).slice(11, 16) || '00:00')"
1092
- @update:model-value="(t2) => t2 && patch({ [metaKey]: `${getStr(metaKey).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T${timeToHHMM(t2)}:00.000Z` })"
1106
+ @update:model-value="(raw) => {
1107
+ const t2 = asSingleTime(raw);
1108
+ if (t2) patch({ [metaKey]: `${getStr(metaKey).slice(0, 10) || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}T${timeToHHMM(t2)}:00.000Z` });
1109
+ }"
1093
1110
  />
1094
1111
  </div>
1095
1112
  </template>
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/vue-3';
2
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;
@@ -0,0 +1,120 @@
1
+ <script setup>
2
+ import { ref, computed, watch, watchEffect } from "vue";
3
+ import { NodeViewWrapper } from "@tiptap/vue-3";
4
+ import { sanitizeSvg } from "../../utils/sanitizeSvg";
5
+ const props = defineProps({
6
+ decorations: { type: Array, required: true },
7
+ selected: { type: Boolean, required: true },
8
+ updateAttributes: { type: Function, required: true },
9
+ deleteNode: { type: Function, required: true },
10
+ node: { type: null, required: true },
11
+ view: { type: null, required: true },
12
+ getPos: { type: null, required: true },
13
+ innerDecorations: { type: null, required: true },
14
+ editor: { type: Object, required: true },
15
+ extension: { type: Object, required: true },
16
+ HTMLAttributes: { type: Object, required: true }
17
+ });
18
+ const rawSvg = computed(() => props.node.attrs.svg || "");
19
+ const title = computed(() => props.node.attrs.title || "");
20
+ const hasSvg = computed(() => !!rawSvg.value);
21
+ const sanitized = ref("");
22
+ const isDragOver = ref(false);
23
+ watchEffect(async () => {
24
+ sanitized.value = await sanitizeSvg(rawSvg.value);
25
+ });
26
+ const containerStyle = computed(() => {
27
+ const s = {};
28
+ if (props.node.attrs.width) s.width = String(props.node.attrs.width);
29
+ if (props.node.attrs.height) s.height = String(props.node.attrs.height);
30
+ return s;
31
+ });
32
+ function isSvgFile(file) {
33
+ return file.type === "image/svg+xml" || file.name.toLowerCase().endsWith(".svg");
34
+ }
35
+ async function loadSvgFile(file) {
36
+ const text = await file.text();
37
+ if (!text.includes("<svg")) return;
38
+ props.updateAttributes({
39
+ svg: text,
40
+ title: props.node.attrs.title || file.name.replace(/\.svg$/i, "")
41
+ });
42
+ }
43
+ function pickSvgFile() {
44
+ const input = document.createElement("input");
45
+ input.type = "file";
46
+ input.accept = ".svg,image/svg+xml";
47
+ input.onchange = () => {
48
+ const file = input.files?.[0];
49
+ if (file) loadSvgFile(file);
50
+ };
51
+ input.click();
52
+ }
53
+ function onDrop(e) {
54
+ e.preventDefault();
55
+ e.stopPropagation();
56
+ isDragOver.value = false;
57
+ const file = Array.from(e.dataTransfer?.files ?? []).find(isSvgFile);
58
+ if (file) loadSvgFile(file);
59
+ }
60
+ function onDragOver(e) {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ isDragOver.value = true;
64
+ }
65
+ function onDragLeave() {
66
+ isDragOver.value = false;
67
+ }
68
+ const placeholderText = computed(() => {
69
+ if (isDragOver.value) return "Drop SVG file here";
70
+ if (hasSvg.value && !sanitized.value) return "SVG removed by sanitizer";
71
+ return "Click or drop an SVG file";
72
+ });
73
+ </script>
74
+
75
+ <template>
76
+ <NodeViewWrapper
77
+ class="svg-embed-wrapper my-3"
78
+ data-type="svg-embed"
79
+ >
80
+ <div
81
+ contenteditable="false"
82
+ data-drag-handle
83
+ class="border border-(--ui-border) rounded-md overflow-hidden transition-colors"
84
+ :class="{
85
+ 'border-(--ui-primary) bg-(--ui-primary)/5': isDragOver,
86
+ 'border-(--ui-primary)': props.selected && !isDragOver
87
+ }"
88
+ @drop="onDrop"
89
+ @dragover="onDragOver"
90
+ @dragleave="onDragLeave"
91
+ >
92
+ <div
93
+ v-if="title && sanitized"
94
+ class="px-3 py-1.5 text-xs font-medium text-(--ui-text-dimmed) border-b border-(--ui-border) bg-(--ui-bg-elevated)"
95
+ >
96
+ {{ title }}
97
+ </div>
98
+ <div
99
+ v-if="sanitized"
100
+ class="flex items-center justify-center p-2 [&_svg]:max-w-full [&_svg]:h-auto"
101
+ :style="containerStyle"
102
+ v-html="sanitized"
103
+ />
104
+ <div
105
+ v-else
106
+ class="flex flex-col items-center justify-center gap-2 px-6 py-10 cursor-pointer text-(--ui-text-dimmed) hover:bg-(--ui-bg-elevated)/40 transition-colors"
107
+ role="button"
108
+ tabindex="0"
109
+ @click="pickSvgFile"
110
+ @keydown.enter="pickSvgFile"
111
+ >
112
+ <UIcon
113
+ name="i-lucide-image"
114
+ class="size-6"
115
+ />
116
+ <span class="text-sm">{{ placeholderText }}</span>
117
+ </div>
118
+ </div>
119
+ </NodeViewWrapper>
120
+ </template>
@@ -0,0 +1,4 @@
1
+ import type { NodeViewProps } from '@tiptap/vue-3';
2
+ declare const __VLS_export: import("vue").DefineComponent<NodeViewProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<NodeViewProps> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;