mnfst 0.5.225 → 0.5.226

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.
@@ -255,6 +255,19 @@ function initializeAuthStore() {
255
255
  'deviceBrand', 'deviceModel', 'countryCode', 'countryName'
256
256
  ];
257
257
 
258
+ // User id in a mirrored state ('' / null when signed out)
259
+ function mirroredUserId(raw) {
260
+ try { const saved = JSON.parse(raw || 'null'); return saved?.isAuthenticated ? (saved.user?.$id || null) : null; } catch { return null; }
261
+ }
262
+
263
+ // Identity this tab is showing data for: while unverified, the last one mirrored
264
+ function shownUserId(store, mirrored) {
265
+ if (store.user?.$id) return store.user.$id;
266
+ if (!store._sessionUnverified) return null;
267
+ if (mirrored === undefined) { try { mirrored = localStorage.getItem(STORAGE_KEY); } catch { mirrored = null; } }
268
+ return mirroredUserId(mirrored);
269
+ }
270
+
258
271
  function sanitizeSessionForStorage(session) {
259
272
  if (!session || typeof session !== 'object') return session;
260
273
  const safe = {};
@@ -271,7 +284,8 @@ function initializeAuthStore() {
271
284
  const state = JSON.parse(e.newValue);
272
285
  const store = Alpine.store('auth');
273
286
  if (store) {
274
- const prevId = store.user?.$id || null;
287
+ const prevId = shownUserId(store, e.oldValue);
288
+ const wasAuthenticated = store.isAuthenticated === true;
275
289
  // Update store state from other tab
276
290
  store._authEpoch++;
277
291
  if (state.isAuthenticated) markVerified(store);
@@ -283,6 +297,8 @@ function initializeAuthStore() {
283
297
  store.magicLinkExpired = state.magicLinkExpired || false;
284
298
  store.otpSent = state.otpSent || false;
285
299
  store.otpExpired = state.otpExpired || false;
300
+ // A code row shown here must be verifiable here
301
+ if (state.otpSent && state.otpUserId) store._otpUserId = state.otpUserId;
286
302
  store.error = state.error;
287
303
  // Identity ended or changed in another tab: scoped data must drop this tab's rows
288
304
  const nextId = state.isAuthenticated ? (state.user?.$id || null) : null;
@@ -292,6 +308,9 @@ function initializeAuthStore() {
292
308
  store.currentTeam = null;
293
309
  }
294
310
  window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
311
+ } else if (nextId && !wasAuthenticated) {
312
+ // Signed in from another tab: this tab's identity-bound data reloads
313
+ window.dispatchEvent(new CustomEvent(state.isAnonymous ? 'manifest:auth:anonymous' : 'manifest:auth:login', { detail: { user: state.user, crossTab: true } }));
295
314
  }
296
315
  }
297
316
  } catch (error) {
@@ -314,6 +333,7 @@ function initializeAuthStore() {
314
333
  magicLinkExpired: store.magicLinkExpired,
315
334
  otpSent: store.otpSent,
316
335
  otpExpired: store.otpExpired,
336
+ otpUserId: store.otpSent ? store._otpUserId : null,
317
337
  error: store.error
318
338
  };
319
339
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
@@ -335,6 +355,7 @@ function initializeAuthStore() {
335
355
  otpExpired: false, // OTP (email/phone): the entered code was wrong/expired
336
356
  otpPhrase: null, // Email OTP: security phrase to display (when enabled; phone has none)
337
357
  _otpUserId: null, // OTP: userId returned by createEmailToken/createPhoneToken, used by verifyOTP
358
+ _otpMigrationTicket: null, // OTP: { ticket, userId } guest-migration ticket, kept across failed verify attempts
338
359
  teams: [], // List of user's teams
339
360
  currentTeam: null, // Currently selected/active team
340
361
  _teamsPollInterval: null, // Interval ID for teams polling (deprecated, use realtime instead)
@@ -713,9 +734,12 @@ function initializeAuthStore() {
713
734
  } catch (error) {
714
735
  if (!current()) return;
715
736
  if (!isNoSession(error)) { scheduleRecheck(this); return; }
737
+ const prevId = shownUserId(this);
716
738
  this._clearIdentity();
717
739
  markVerified(this);
718
740
  syncStateToStorage(this);
741
+ // Rows shown for the last-known user drop
742
+ if (prevId) window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
719
743
  if (this._guestAuto && this._createAnonymousSession) await this._createAnonymousSession();
720
744
  return;
721
745
  } finally {
@@ -907,6 +931,9 @@ function initializeAuthStore() {
907
931
  return { success: false, error: 'Appwrite not configured' };
908
932
  }
909
933
 
934
+ // A ticket kept from a failed guest OTP must not outlive an explicit logout
935
+ this._otpMigrationTicket = null;
936
+
910
937
  // If not authenticated, nothing to logout from
911
938
  if (!this.isAuthenticated) {
912
939
  return { success: true };
@@ -1080,12 +1107,18 @@ function initializeAuthStore() {
1080
1107
  return this.user;
1081
1108
  } catch (error) {
1082
1109
  if (!isNoSession(error)) throw error;
1110
+ const prevId = shownUserId(this);
1083
1111
  markVerified(this);
1084
1112
  this.isAuthenticated = false;
1085
1113
  this.isAnonymous = false;
1086
1114
  this.user = null;
1087
1115
  this.session = null;
1088
1116
  syncStateToStorage(this);
1117
+ if (prevId) {
1118
+ this.teams = [];
1119
+ this.currentTeam = null;
1120
+ window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared'));
1121
+ }
1089
1122
  throw error;
1090
1123
  }
1091
1124
  }
@@ -6474,26 +6507,33 @@ function initializeEmailOTP() {
6474
6507
 
6475
6508
  this.inProgress = true;
6476
6509
  this.error = null;
6510
+ let guestEnded = false;
6477
6511
 
6478
6512
  try {
6479
6513
  const appwriteConfig = await config.getAppwriteConfig();
6480
6514
  const wasGuest = this.isAnonymous;
6481
6515
 
6482
- // Guest team carryover: issue the migration ticket now, while the guest
6483
- // session is still authenticated; redeemed after the new session exists.
6484
- let migrationTicket = null;
6516
+ // Guest team carryover: issue the migration ticket while the guest session
6517
+ // still authenticates. Kept on the store, bound to this code's account, so a
6518
+ // retry after a wrong code redeems it and no other sign-in can.
6485
6519
  if (wasGuest && appwriteConfig?.guestMigrationFunctionId && this._callGuestMigration) {
6486
6520
  const prep = await this._callGuestMigration('/prepare', {});
6487
- if (prep?.ok && prep.ticket) migrationTicket = prep.ticket;
6521
+ if (prep?.ok && prep.ticket) {
6522
+ this._otpMigrationTicket = { ticket: prep.ticket, userId: this._otpUserId };
6523
+ }
6488
6524
  }
6525
+ const kept = this._otpMigrationTicket;
6526
+ const migrationTicket = kept?.userId === this._otpUserId ? kept.ticket : null;
6489
6527
 
6490
- // Appwrite can't convert anonymous accounts via OTP, so delete the
6491
- // guest session first to avoid a "session prohibited" conflict.
6528
+ // Appwrite refuses createSession while any session is active, before it
6529
+ // checks the code, so the guest session must go first.
6492
6530
  if (this.session && this.isAnonymous) {
6493
6531
  try {
6494
6532
  await this._appwrite.account.deleteSession(this.session.$id);
6533
+ guestEnded = true;
6495
6534
  } catch (deleteError) {
6496
- // Could not delete anonymous session
6535
+ const status = deleteError?.code || deleteError?.statusCode;
6536
+ guestEnded = status === 401 || status === 404;
6497
6537
  }
6498
6538
  }
6499
6539
 
@@ -6516,6 +6556,7 @@ function initializeEmailOTP() {
6516
6556
 
6517
6557
  // Redeem the migration ticket as the new account to carry teams over.
6518
6558
  // Best-effort; failure never blocks the already-successful sign-in.
6559
+ this._otpMigrationTicket = null;
6519
6560
  if (migrationTicket && this._callGuestMigration) {
6520
6561
  await this._callGuestMigration('/commit', { ticket: migrationTicket });
6521
6562
  }
@@ -6541,7 +6582,7 @@ function initializeEmailOTP() {
6541
6582
  } catch (error) {
6542
6583
  const errorMessage = error.message || '';
6543
6584
  const errorCode = error.code || error.statusCode || '';
6544
- const isExpiredOrInvalid = errorMessage && (
6585
+ const isExpiredOrInvalid = error.type !== 'user_session_already_exists' && errorMessage && (
6545
6586
  errorMessage.includes('expired') ||
6546
6587
  errorMessage.includes('Invalid token') ||
6547
6588
  errorMessage.includes('invalid') ||
@@ -6551,14 +6592,30 @@ function initializeEmailOTP() {
6551
6592
 
6552
6593
  this.otpExpired = !!isExpiredOrInvalid;
6553
6594
  this.error = isExpiredOrInvalid ? null : error.message;
6554
- this.isAuthenticated = false;
6555
- this.isAnonymous = false;
6595
+
6596
+ // Guest session is gone though sign-in failed: drop its identity and teams.
6597
+ // otpSent, the code's userId and the migration ticket stay for a retry.
6598
+ if (guestEnded) {
6599
+ this.isAuthenticated = false;
6600
+ this.isAnonymous = false;
6601
+ this.user = null;
6602
+ this.session = null;
6603
+ if (this._resetTeamsState) {
6604
+ this._resetTeamsState();
6605
+ }
6606
+ }
6556
6607
 
6557
6608
  if (this._syncStateToStorage) {
6558
6609
  this._syncStateToStorage(this);
6559
6610
  }
6560
6611
 
6561
- return { success: false, error: error.message };
6612
+ if (guestEnded) {
6613
+ window.dispatchEvent(new CustomEvent('manifest:auth:session-cleared', {
6614
+ detail: { reason: 'otp-failed' }
6615
+ }));
6616
+ }
6617
+
6618
+ return { success: false, error: error.message, guestEnded };
6562
6619
  } finally {
6563
6620
  this.inProgress = false;
6564
6621
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.225';
5
+ const MANIFEST_BUILD_VERSION = '0.5.226';
6
6
 
7
7
  /* Manifest Data Sources - Appwrite Integration */
8
8
 
package/lib/manifest.d.ts CHANGED
@@ -434,7 +434,7 @@ export interface ManifestTextEdit {
434
434
  export type ManifestDirective =
435
435
  | 'x-route' | 'x-anchors'
436
436
  | 'x-icon' | 'x-svg' | 'x-markdown' | 'x-code' | 'x-code-group'
437
- | 'x-toast' | 'x-tooltip' | 'x-carousel' | 'x-virtual' | 'x-resize' | 'x-computed' | 'x-defer'
437
+ | 'x-toast' | 'x-tooltip' | 'x-carousel' | 'x-virtual' | 'x-computed' | 'x-defer'
438
438
  | 'x-chart' | 'x-date' | 'x-color' | 'x-colorpicker' | 'x-combobox'
439
439
  | 'x-export' | 'x-import' | 'x-pay' | 'x-files' | 'x-data-files'
440
440
  | 'x-edit' | 'x-text-edit';
@@ -2,7 +2,7 @@
2
2
 
3
3
  (function () {
4
4
 
5
- const MANIFEST_BUILD_VERSION = '0.5.225';
5
+ const MANIFEST_BUILD_VERSION = '0.5.226';
6
6
 
7
7
  /* Manifest Data Sources - Configuration */
8
8
 
@@ -1593,6 +1593,7 @@ window.ManifestDataStore = {
1593
1593
  scope: '',
1594
1594
  generation: 0,
1595
1595
  pending: new Map(), // source -> time its debounced write is due
1596
+ landedIdentity: new Map(), // source -> identity its pending rows were read under
1596
1597
  writeTimer: null,
1597
1598
  hydrated: new Set(), // sources hydrated (or attempted) this generation
1598
1599
  hydratedIdentity: null, // identity auth-dependent hydration last ran under
@@ -1609,10 +1610,10 @@ window.ManifestDataStore = {
1609
1610
  const liveLocale = () => (typeof document !== 'undefined' && document.documentElement?.lang)
1610
1611
  || (typeof Alpine !== 'undefined' && Alpine.store?.('locale')?.current) || 'en';
1611
1612
 
1612
- // Auth-dependent sources (scope / $auth. queries) carry the identity they were read under
1613
+ // Auth-dependent sources (Appwrite tables/buckets, scope / $auth. queries) carry the identity they were read under
1613
1614
  function isAuthDependent(source) {
1614
1615
  if (!source || typeof source !== 'object') return false;
1615
- if (source.scope) return true;
1616
+ if (source.scope || source.appwriteTableId || source.appwriteBucketId) return true;
1616
1617
  try { return JSON.stringify(source.queries || '').includes('$auth.'); } catch { return false; }
1617
1618
  }
1618
1619
 
@@ -1629,6 +1630,19 @@ window.ManifestDataStore = {
1629
1630
  return auth?._sessionUnverified === true;
1630
1631
  }
1631
1632
 
1633
+ // Auth's last recorded identity (cross-tab mirror): '' signed out, null unknown
1634
+ function lastKnownIdentity() {
1635
+ try {
1636
+ const saved = JSON.parse((typeof window !== 'undefined' && window.localStorage?.getItem('manifest:auth:state')) || 'null');
1637
+ if (!saved || typeof saved !== 'object') return null;
1638
+ return saved.isAuthenticated ? (String(saved.user?.$id || saved.user?.id || '') || null) : '';
1639
+ } catch { return null; }
1640
+ }
1641
+
1642
+ // Unconfirmed (booting / offline): hydration trusts the last recorded identity; the data reset covers a mismatch
1643
+ function unconfirmed() { return authIdentity() === null || authUnverified(); }
1644
+ function hydrationIdentity() { return unconfirmed() ? lastKnownIdentity() : authIdentity(); }
1645
+
1632
1646
  function warnOnce(key, message, error) {
1633
1647
  if (state.warned.has(key)) return;
1634
1648
  state.warned.add(key);
@@ -1735,6 +1749,7 @@ window.ManifestDataStore = {
1735
1749
  function register(source, config) {
1736
1750
  const cfg = normalizeConfig(config === undefined ? true : config);
1737
1751
  if (!cfg) { state.sources.delete(source); return false; }
1752
+ cfg.authDependent = isAuthDependent(state.manifest?.data?.[source] || state.manifest?.appwrite?.[source]);
1738
1753
  state.sources.set(source, cfg);
1739
1754
  state.hydrated.delete(source);
1740
1755
  state.fetchKicked.delete(source);
@@ -1819,12 +1834,22 @@ window.ManifestDataStore = {
1819
1834
  if (next !== state.scope) changeScope(next);
1820
1835
  }
1821
1836
 
1837
+ // Pending saves of rows read under an identity that has since changed never land
1838
+ function dropStaleWrites() {
1839
+ if (unconfirmed()) return;
1840
+ const identity = authIdentity();
1841
+ for (const [source, landed] of [...state.landedIdentity]) {
1842
+ if (landed !== null && landed !== identity) cancelWrites(source);
1843
+ }
1844
+ }
1845
+
1822
1846
  function watchScope() {
1823
1847
  if (state.watching || typeof window === 'undefined') return;
1824
1848
  state.watching = true;
1825
1849
  for (const type of AUTH_EVENTS) {
1826
1850
  window.addEventListener(type, () => {
1827
1851
  if (!state.enabled) return;
1852
+ dropStaleWrites();
1828
1853
  if (WIPE_EVENTS.has(type)) deleteScope(state.scope);
1829
1854
  refreshScope();
1830
1855
  if (!state.scopePending) hydrate(bootSources()).catch(() => { /* disabled */ });
@@ -1998,14 +2023,18 @@ window.ManifestDataStore = {
1998
2023
  return null;
1999
2024
  }
2000
2025
 
2001
- function snapshotRecord(source) {
2026
+ function snapshotRecord(source, landed) {
2002
2027
  const cfg = state.sources.get(source);
2003
2028
  const ds = dataStore();
2004
2029
  if (!cfg || !ds) return null;
2005
2030
  const raw = ds.getRawData(source);
2006
2031
  if (raw === null || raw === undefined) return null;
2007
- const identity = cfg.authDependent ? authIdentity() : undefined;
2008
- if (identity === null) return null;
2032
+ let identity;
2033
+ if (cfg.authDependent) {
2034
+ // Stamped with the identity the rows were read under; skipped if it changed since or was never known
2035
+ identity = authUnverified() ? null : authIdentity();
2036
+ if (identity === null || landed === null || (landed !== undefined && landed !== identity)) return null;
2037
+ }
2009
2038
  try {
2010
2039
  const snapshot = snapshotOf(source, raw, cfg);
2011
2040
  if (snapshot === null) return null;
@@ -2028,9 +2057,10 @@ window.ManifestDataStore = {
2028
2057
 
2029
2058
  // Every source whose debounce has elapsed is written in ONE transaction
2030
2059
  function flush(sources) {
2031
- for (const source of sources) state.pending.delete(source);
2060
+ const landedIdentity = new Map(sources.map(source => [source, state.landedIdentity.get(source)]));
2061
+ for (const source of sources) { state.pending.delete(source); state.landedIdentity.delete(source); }
2032
2062
  if (!state.enabled || state.disabled) return Promise.resolve();
2033
- const records = sources.map(snapshotRecord).filter(Boolean);
2063
+ const records = sources.map(source => snapshotRecord(source, landedIdentity.get(source))).filter(Boolean);
2034
2064
  if (!records.length) return Promise.resolve();
2035
2065
  const generation = state.generation;
2036
2066
  return withStore('readwrite', (store) => { for (const record of records) store.put(record); }).then(() => {
@@ -2062,8 +2092,9 @@ window.ManifestDataStore = {
2062
2092
  }
2063
2093
 
2064
2094
  function cancelWrites(source) {
2065
- if (source !== undefined) { state.pending.delete(source); return; }
2095
+ if (source !== undefined) { state.pending.delete(source); state.landedIdentity.delete(source); return; }
2066
2096
  state.pending.clear();
2097
+ state.landedIdentity.clear();
2067
2098
  if (state.writeTimer !== null) { clearTimeout(state.writeTimer); state.writeTimer = null; }
2068
2099
  }
2069
2100
 
@@ -2071,7 +2102,12 @@ window.ManifestDataStore = {
2071
2102
  function onLanded(sources) {
2072
2103
  if (!state.enabled || state.disabled || state.scopePending) return; // nothing is keyed under an unresolved scope
2073
2104
  const at = Date.now() + WRITE_DEBOUNCE_MS;
2074
- for (const source of sources) if (state.sources.has(source)) state.pending.set(source, at);
2105
+ const identity = hydrationIdentity();
2106
+ for (const source of sources) {
2107
+ if (!state.sources.has(source)) continue;
2108
+ state.pending.set(source, at);
2109
+ if (state.sources.get(source).authDependent) state.landedIdentity.set(source, identity);
2110
+ }
2075
2111
  if (state.writeTimer !== null) { clearTimeout(state.writeTimer); state.writeTimer = null; }
2076
2112
  scheduleWrites();
2077
2113
  }
@@ -2085,8 +2121,11 @@ window.ManifestDataStore = {
2085
2121
  if (majorMinor(record.frameworkVersion) !== majorMinor(state.frameworkVersion)) return { ok: false, drop: true };
2086
2122
  if (record.locale && record.locale !== liveLocale()) return { ok: false };
2087
2123
  // Another identity's (or an unstamped) snapshot never hydrates: the scope alone may be '' for everyone
2088
- // An unverified (offline) signed-out boot keeps it for the session check to come back
2089
- if (cfg.authDependent && record.identity !== authIdentity()) return authUnverified() ? { ok: false } : { ok: false, drop: true };
2124
+ // Before the session is confirmed (booting / offline) a mismatch is kept for the check to come back
2125
+ if (cfg.authDependent) {
2126
+ const identity = hydrationIdentity();
2127
+ if (identity === null || record.identity !== identity) return unconfirmed() ? { ok: false } : { ok: false, drop: true };
2128
+ }
2090
2129
  return { ok: true };
2091
2130
  }
2092
2131
 
@@ -2095,7 +2134,7 @@ window.ManifestDataStore = {
2095
2134
  async function hydrate(sources) {
2096
2135
  const ds = dataStore();
2097
2136
  // Auth-dependent sources wait for a known identity (re-run on auth events), and retry when it changes
2098
- const identity = authIdentity();
2137
+ const identity = hydrationIdentity();
2099
2138
  const known = identity !== null;
2100
2139
  if (known && identity !== state.hydratedIdentity) {
2101
2140
  for (const [source, cfg] of state.sources) if (cfg.authDependent) state.hydrated.delete(source);
@@ -8666,10 +8705,8 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8666
8705
  }
8667
8706
  return null;
8668
8707
  } else if (method === '$remove') {
8669
- // Convenience method: delete file with automatic bucket reload and entry unlinking
8708
+ // Delete file(s), drop them from the source, unlink from table entries
8670
8709
  // Usage: $x.assets.$remove(fileId)
8671
- // Automatically reloads the entire assets bucket and unlinks from table entries
8672
- // This is an alias for $delete with the same auto-reload functionality
8673
8710
  const fileId = args[0];
8674
8711
  if (!fileId) {
8675
8712
  throw new Error('[Manifest Data] $remove requires file ID');
@@ -8687,20 +8724,15 @@ function createAppwriteMethodsHandler(dataSourceName, reloadDataSource) {
8687
8724
  // Continue with delete even if unlink fails
8688
8725
  }
8689
8726
 
8690
- // Delete file(s) and reload in parallel for speed
8691
- const deletePromise = Array.isArray(fileId)
8692
- ? Promise.all(fileId.map(id => window.ManifestDataAppwrite.deleteFile(bucketId, id.$id || id)))
8693
- : window.ManifestDataAppwrite.deleteFile(bucketId, actualFileId);
8694
-
8727
+ const deletedIds = (Array.isArray(fileId) ? fileId : [fileId]).map(id => id.$id || id);
8695
8728
  const generation = window.ManifestDataStore?.sourceGeneration?.(dataSourceName);
8696
- const reloadPromise = window.ManifestDataAppwrite.listBucketFiles(bucketId, []);
8697
-
8698
- // Do delete and reload in parallel
8699
- const [result, reloadedData] = await Promise.all([deletePromise, reloadPromise]);
8729
+ const result = Array.isArray(fileId)
8730
+ ? await Promise.all(deletedIds.map(id => window.ManifestDataAppwrite.deleteFile(bucketId, id)))
8731
+ : await window.ManifestDataAppwrite.deleteFile(bucketId, actualFileId);
8700
8732
 
8701
- // Network landing: reloaded bucket listing replaces the source (coalesced)
8702
- if (reloadedData && window.ManifestDataStore?.landRows) {
8703
- await window.ManifestDataStore.landRows(dataSourceName, reloadedData, { mode: 'replace', generation });
8733
+ // Drop the deleted file(s) locally: a relisting would bypass the source's queries and scope
8734
+ if (window.ManifestDataStore?.landRemove) {
8735
+ await window.ManifestDataStore.landRemove(dataSourceName, deletedIds, { generation });
8704
8736
  }
8705
8737
 
8706
8738
  // Reload affected table data sources so fileIds arrays and counters
@@ -11489,15 +11521,37 @@ function authSettledUnresolvable(manifest) {
11489
11521
  return auth._initialized === true && auth.isAuthenticated === true && teamsSettled(auth, manifest);
11490
11522
  }
11491
11523
 
11492
- function isAuthDependent(config) {
11524
+ // Rows depend on who reads them: scope / $auth. queries, or any Appwrite table/bucket (row/file permissions).
11525
+ // Reset on identity change only; whether a read needs an identity is buildAppwriteQueries' call (null = wait).
11526
+ function isIdentityBound(config) {
11493
11527
  if (!config || typeof config !== 'object') return false;
11528
+ if (window.ManifestDataConfig?.isAppwriteCollection?.(config)) return true;
11494
11529
  if (window.ManifestDataConfig?.getScope?.(config)) return true;
11495
11530
  try { return JSON.stringify(config.queries || '').includes('$auth.'); } catch { return false; }
11496
11531
  }
11497
11532
 
11498
- // Logout / session cleared: drop the previous identity's rows now, then settle for the new one
11533
+ // Auth's last recorded identity (cross-tab mirror): '' signed out, null unknown
11534
+ function lastKnownIdentity() {
11535
+ try {
11536
+ const saved = JSON.parse(window.localStorage?.getItem('manifest:auth:state') || 'null');
11537
+ if (!saved || typeof saved !== 'object') return null;
11538
+ return saved.isAuthenticated ? (String(saved.user?.$id || saved.user?.id || '') || null) : '';
11539
+ } catch { return null; }
11540
+ }
11541
+
11542
+ // Confirmed identity: '' signed out (or no auth plugin), null while booting or offline-unverified
11543
+ function confirmedIdentity() {
11544
+ const auth = currentAuth();
11545
+ if (!auth) return '';
11546
+ if (auth._initialized !== true || (auth._sessionUnverified === true && auth.isAuthenticated !== true)) return null;
11547
+ return auth.isAuthenticated === true ? (authUserId(auth) || '') : '';
11548
+ }
11549
+
11550
+ // Identity change (sign-in, sign-out, account switch): identity-bound sources reload for the new identity.
11551
+ // A previous user's rows drop at once; rows read signed out stay live until the reload lands.
11499
11552
  function setupAuthResetListener() {
11500
- const reset = async () => {
11553
+ let bound = (currentAuth() ? confirmedIdentity() : null) ?? lastKnownIdentity();
11554
+ const reset = async (clear) => {
11501
11555
  const manifest = await window.ManifestDataConfig?.ensureManifest?.();
11502
11556
  const ds = window.ManifestDataStore;
11503
11557
  const store = typeof Alpine !== 'undefined' ? Alpine.store('data') : null;
@@ -11506,14 +11560,27 @@ function setupAuthResetListener() {
11506
11560
  const configs = { ...(manifest.appwrite || {}), ...(manifest.data || {}) };
11507
11561
  const locale = liveLocale();
11508
11562
  for (const [name, config] of Object.entries(configs)) {
11509
- if (!isAuthDependent(config) || raw[`_${name}_state`] === undefined) continue;
11510
- window.ManifestDataRealtime?.unsubscribeFromDataSource?.(name);
11511
- ds.resetSource(name);
11563
+ if (!isIdentityBound(config) || raw[`_${name}_state`] === undefined) continue;
11564
+ if (clear) {
11565
+ window.ManifestDataRealtime?.unsubscribeFromDataSource?.(name);
11566
+ ds.resetSource(name);
11567
+ }
11512
11568
  loadDataSource(name, locale, { reload: true });
11513
11569
  }
11514
11570
  };
11515
- window.addEventListener('manifest:auth:logout', reset);
11516
- window.addEventListener('manifest:auth:session-cleared', reset);
11571
+ const onAuth = (type) => {
11572
+ const ended = type === 'manifest:auth:logout' || type === 'manifest:auth:session-cleared';
11573
+ const next = confirmedIdentity() ?? (ended ? '' : null);
11574
+ if (next === null) return;
11575
+ const prev = bound;
11576
+ bound = next;
11577
+ if (prev === next) return;
11578
+ // Unknown before: rows so far were read under this same session (a sign-out still drops them)
11579
+ if (prev === null && !ended) return;
11580
+ reset(prev !== '');
11581
+ };
11582
+ ['manifest:auth:logout', 'manifest:auth:session-cleared', 'manifest:auth:login', 'manifest:auth:anonymous', 'manifest:auth:initialized']
11583
+ .forEach(type => window.addEventListener(type, () => onAuth(type)));
11517
11584
  }
11518
11585
 
11519
11586
  // Client-side scope filter: Appwrite returns all accessible files, so narrow to
@@ -1,6 +1,6 @@
1
1
  {
2
- "manifest.appwrite.auth.js": "sha384-eJ46LQV0lAoY48Zy1J8iecrK9azn/407FdeXz9+dwsuAJESUugOzlEBdTCw+i3m5",
3
- "manifest.appwrite.data.js": "sha384-uWy++AIY+DZbiEm+Qhkqxc6OJ3E0NSzx5Mq+8TCaAAI+cCV6PSbrS84JRjYV2jNF",
2
+ "manifest.appwrite.auth.js": "sha384-Ek2fHprBtcIFHAkOHtELD40gjfME3lWdmIQGB7h8+gcm7hn5QJzt9DRpo9huz4BE",
3
+ "manifest.appwrite.data.js": "sha384-bwfWutqQz9CnVgvXIKCR9VZrRQ3ytswJ6zvKOevhLobHOaryZazSAr4tFRurawiZ",
4
4
  "manifest.appwrite.presences.js": "sha384-JyVDWFT69b7Svq8J8p9tNWe2v9PZNQBY5UyhtOl1vnuHFAX0CRQ3MA9SiKq5wzNh",
5
5
  "manifest.bindings.js": "sha384-lPtMf8Ykaic9gNVZKq8Scia1881ZeMAyxer3WCIkUNwwrgzGBWto1OXADUwjvORF",
6
6
  "manifest.charts.js": "sha384-gEAv/98+29RbhvNkiNWTNyBPbLTFr2Ld15wLu29FLCL57j1cwGChRrL9Pfk7dBtT",
@@ -11,7 +11,7 @@
11
11
  "manifest.combobox.js": "sha384-MILTkrgsTzsYUjGo4ZuADqV85Xs4GmfB4xexfe9vbJMIoQTZ42W4zs+DTaVSPEsx",
12
12
  "manifest.components.js": "sha384-4bc3HgPvF9DHMiOG08w+nWXI7Hho01RuAPAUL9xyLxOiS2igeZpulCVcvr5aT9v6",
13
13
  "manifest.computed.js": "sha384-GYML/e81gQfN+iDbqhPUgny8bINCyL+SrbTofoGk4hlCnLLjv51mCv42JMqq+ku5",
14
- "manifest.data.js": "sha384-YljhDHjZ6BCU7qjoeecMvG1mgC7FwR7vw1EWBAyd6ZcMAlc3nmiztv3swRckN320",
14
+ "manifest.data.js": "sha384-Dtpxer0CayxZCsGD8zCe8lW4MwsnaWA7Yxu1cWBxPQI1PJHh6qCwEJejHYt2go3F",
15
15
  "manifest.datepicker.js": "sha384-XnbeURni2X91ozVzatQcSjYYIS4MoW12dBRJnYAGBr0nCQ624heH5s+xtQSpLZX6",
16
16
  "manifest.defer.js": "sha384-WcCf8YQ1bKIXCLSZ24a0bJwepbYntYc8TzQM50hklo5VMIH8AtzDnwl6WU4prRv1",
17
17
  "manifest.device.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
@@ -23,12 +23,11 @@
23
23
  "manifest.markdown.js": "sha384-JWUxlUZADxk1hZY7NpDX051UhtXt/ofpfsCDBRcweU6JhLbN8B832yPLgCatxFdl",
24
24
  "manifest.native.js": "sha384-g+txmrEszYtuOvRelTp/c1wSNR2AtllVI9rx5NPPD7DzhVUnMNughonZykv2xlBW",
25
25
  "manifest.payments.js": "sha384-8pofk2zhxtapPMpwnFyTelPMrYhShNhDCE722uQh/EYFvZFuPLmSdgunqsarVQjv",
26
- "manifest.resize.js": "sha384-+0IsyrGP/4gZmNbD6oU8dCgEyhpBsXMm74NIenYeUGuGRWyJDurcG27xE/CA0/aA",
27
26
  "manifest.router.js": "sha384-xkQaHO60lMQwtnqGPxPJJ646eyqBjINs4AFesrQbe2dWqPIoIcJ9RD5wsjqjak7w",
28
27
  "manifest.slides.js": "sha384-3uRTkyK9XPLmnxI2+igZlpi4EyPlU/7IHj5j3BZJJ2KN455vXyk99fiXV3feO/XY",
29
28
  "manifest.status.js": "sha384-MBO345ab7H6Uq1X2dZLt4Ge1Gcra+Z2qYApvlAeIV8CJhDDJrz2FPXpp+Kcp8zcM",
30
29
  "manifest.svg.js": "sha384-qr/8H9+2cIhSXNbRWRtIdrDlTOxKlNJqwEefHvCYoiBNW/kKhPTFa109Orv/EBnb",
31
- "manifest.sw.js": "sha384-hEYCjpYM1VHGJLc/RnSyUsmhBx/XrGhxCA31U6kW/TpFadcUZ0Uz96Rqm0uFbr+I",
30
+ "manifest.sw.js": "sha384-e59BjfTqiJXTPdMgK3bkvYA2mzha1WEQeLgPABSDg35IPb+5Zd0BMUvND2kxrxoc",
32
31
  "manifest.tabs.js": "sha384-OIIcyaQnGS0eNYk4zgAFEDboNYect7THvUd9D0V6nqCeDOxQcoiqLrBEmmWVTTet",
33
32
  "manifest.tailwind.js": "sha384-r/Jvqb+Piz21jUts10BbVAJoKkShOdJRVFpywyi4HPIi/OfOJq5CtPyWAPbdkMgt",
34
33
  "manifest.text.edit.js": "sha384-RzOvLQE8GbmI6gntAwiotDewYT/08OHcd7sJ5hLon8e8u1Xbq1D7VfEGuaRVd2CE",
@@ -16,7 +16,7 @@
16
16
 
17
17
  // Build stamps the published version here; unstamped (dev) falls back to the
18
18
  // registration URL's `v`, then 'dev'.
19
- const BUILD_VERSION = '0.5.225';
19
+ const BUILD_VERSION = '0.5.226';
20
20
  const CACHE_PREFIX = 'mnfst-sw:';
21
21
  const META_CACHE = CACHE_PREFIX + 'meta';
22
22
  // Sized to hold a full precache.json warm-up (PRECACHE_MAX_ENTRIES server-side)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst",
3
- "version": "0.5.225",
3
+ "version": "0.5.226",
4
4
  "private": false,
5
5
  "workspaces": [
6
6
  "templates/starter",
@@ -47,7 +47,8 @@
47
47
  "test:e2e:sdk": "node tests/e2e/payments.sdk.e2e.mjs",
48
48
  "test:e2e:sw": "node tests/e2e/sw.e2e.mjs",
49
49
  "lint": "echo 'No linting configured'",
50
- "release:docs": "node scripts/release-bump.mjs packages/docs && cd packages/docs && npm publish --auth-type=web"
50
+ "release:docs": "node scripts/release-bump.mjs packages/docs && cd packages/docs && npm publish --auth-type=web",
51
+ "release:check": "node scripts/release-check.mjs"
51
52
  },
52
53
  "devDependencies": {
53
54
  "@rollup/plugin-commonjs": "^28.0.1",
@@ -1,194 +0,0 @@
1
- /* Manifest Resize Handles
2
-
3
- Superseded by x-edit.size (manifest.edit.css) — kept for existing projects.
4
- Self-contained: handle geometry lives here, not in a shared snippet. */
5
-
6
- @layer base, components, utilities;
7
-
8
- @layer utilities {
9
-
10
- :where([x-resize]) {
11
- position: relative;
12
-
13
- /* Resize handle area */
14
- .resize-handle {
15
- position: absolute;
16
- display: block;
17
- width: var(--h);
18
- height: var(--h);
19
- z-index: 100;
20
- background: transparent;
21
- border: none;
22
- outline: none;
23
- --h: var(--spacing-resize-handle, 1rem);
24
-
25
- /* Visible handle */
26
- &::before {
27
- content: '';
28
- position: absolute;
29
- top: 50%;
30
- left: 50%;
31
- transform: translate(-50%, -50%);
32
- width: 1px;
33
- height: 1px;
34
- background: transparent;
35
- transition: background-color 0.2s ease;
36
- pointer-events: none
37
- }
38
-
39
- &:hover::before,
40
- &:active::before {
41
- background-color: var(--color-line, color-mix(in oklch, oklch(20.5% 0 0) 10%, transparent));
42
- }
43
- }
44
-
45
- /* Handle geometry — edges span their axis, corners stay square */
46
- .resize-handle-top,
47
- .resize-handle-bottom {
48
- left: 0;
49
- width: 100%;
50
- height: var(--h);
51
- cursor: ns-resize;
52
-
53
- &::before {
54
- width: 100%
55
- }
56
- }
57
-
58
- .resize-handle-left,
59
- .resize-handle-right,
60
- .resize-handle-start,
61
- .resize-handle-end {
62
- top: 0;
63
- width: var(--h);
64
- height: 100%;
65
- cursor: ew-resize;
66
-
67
- &::before {
68
- height: 100%
69
- }
70
- }
71
-
72
- .resize-handle-top {
73
- top: calc(var(--h) * -0.5)
74
- }
75
-
76
- .resize-handle-bottom {
77
- bottom: calc(var(--h) * -0.5)
78
- }
79
-
80
- .resize-handle-left,
81
- .resize-handle-top-left,
82
- .resize-handle-bottom-left {
83
- left: calc(var(--h) * -0.5)
84
- }
85
-
86
- .resize-handle-right,
87
- .resize-handle-top-right,
88
- .resize-handle-bottom-right {
89
- right: calc(var(--h) * -0.5)
90
- }
91
-
92
- /* Logical edges — inset-inline-* auto-flips for RTL */
93
- .resize-handle-start,
94
- .resize-handle-top-start,
95
- .resize-handle-bottom-start {
96
- inset-inline-start: calc(var(--h) * -0.5)
97
- }
98
-
99
- .resize-handle-end,
100
- .resize-handle-top-end,
101
- .resize-handle-bottom-end {
102
- inset-inline-end: calc(var(--h) * -0.5)
103
- }
104
-
105
- .resize-handle-top-left,
106
- .resize-handle-top-right,
107
- .resize-handle-top-start,
108
- .resize-handle-top-end {
109
- top: calc(var(--h) * -0.5)
110
- }
111
-
112
- .resize-handle-bottom-left,
113
- .resize-handle-bottom-right,
114
- .resize-handle-bottom-start,
115
- .resize-handle-bottom-end {
116
- bottom: calc(var(--h) * -0.5)
117
- }
118
-
119
- /* Corner cursors */
120
- .resize-handle-top-left,
121
- .resize-handle-top-start {
122
- cursor: nw-resize;
123
- }
124
-
125
- .resize-handle-top-right,
126
- .resize-handle-top-end {
127
- cursor: ne-resize;
128
- }
129
-
130
- .resize-handle-bottom-left,
131
- .resize-handle-bottom-start {
132
- cursor: sw-resize;
133
- }
134
-
135
- .resize-handle-bottom-right,
136
- .resize-handle-bottom-end {
137
- cursor: se-resize;
138
- }
139
-
140
- /* Screen overlay to get mouse coordinates */
141
- .resize-overlay {
142
- position: fixed;
143
- top: 0;
144
- left: 0;
145
- width: 100vw;
146
- height: 100vh;
147
- z-index: 9999;
148
- display: none;
149
- background: transparent
150
- }
151
-
152
- /* Resize states */
153
- .resizable-closing {
154
- opacity: 0.5;
155
- transition: opacity 0.2s ease
156
- }
157
-
158
- .resizable-closed {
159
- display: none !important
160
- }
161
-
162
- /* Focus states for accessibility — framework ring (manifest.theme.css) */
163
- .resize-handle:focus {
164
- outline: none;
165
- box-shadow: 0 0 0 2px color-mix(in oklch, var(--color-content-stark) 30%, transparent)
166
- }
167
-
168
- /* Reduced motion support */
169
- @media (prefers-reduced-motion: reduce) {
170
-
171
- :where(.resizable-closing,
172
- .resize-handle) {
173
- transition: none
174
- }
175
- }
176
- }
177
-
178
- /* RTL cursor flips — placement follows the logical insets in the shared geometry */
179
- [dir=rtl] :where([x-resize]) .resize-handle-top-start {
180
- cursor: ne-resize;
181
- }
182
-
183
- [dir=rtl] :where([x-resize]) .resize-handle-top-end {
184
- cursor: nw-resize;
185
- }
186
-
187
- [dir=rtl] :where([x-resize]) .resize-handle-bottom-start {
188
- cursor: se-resize;
189
- }
190
-
191
- [dir=rtl] :where([x-resize]) .resize-handle-bottom-end {
192
- cursor: sw-resize;
193
- }
194
- }
@@ -1,488 +0,0 @@
1
- /* Manifest Resizer */
2
-
3
- (function () {
4
-
5
-
6
- function initializeResizablePlugin() {
7
- // Cache for unit conversions to avoid repeated DOM manipulation
8
- const unitCache = new Map();
9
- let tempConversionEl = null;
10
-
11
- // Helper to parse value and unit from CSS dimension
12
- const parseDimension = (value) => {
13
- if (typeof value === 'number') return { value, unit: 'px' };
14
- const match = String(value).match(/^([\d.]+)(.*)$/);
15
- return match ? { value: parseFloat(match[1]), unit: match[2] || 'px' } : { value: 0, unit: 'px' };
16
- };
17
-
18
- // Helper to convert any unit to pixels (cached and optimized)
19
- const convertToPixels = (value, unit) => {
20
- if (unit === 'px') return value;
21
-
22
- const cacheKey = `${value}${unit}`;
23
- if (unitCache.has(cacheKey)) {
24
- return unitCache.get(cacheKey);
25
- }
26
-
27
- // Use a single cached conversion element
28
- if (!tempConversionEl) {
29
- tempConversionEl = document.createElement('div');
30
- tempConversionEl.style.cssText = 'visibility:hidden;position:absolute;top:-9999px;left:-9999px;width:0;height:0;';
31
- document.body.appendChild(tempConversionEl);
32
- }
33
-
34
- tempConversionEl.style.width = `${value}${unit}`;
35
- const pixels = tempConversionEl.getBoundingClientRect().width;
36
-
37
- unitCache.set(cacheKey, pixels);
38
- return pixels;
39
- };
40
-
41
- // Helper to convert pixels back to original unit (cached)
42
- const convertFromPixels = (pixels, unit, element) => {
43
- if (unit === 'px') return pixels;
44
-
45
- const cacheKey = `${pixels}${unit}${element.tagName}`;
46
- if (unitCache.has(cacheKey)) {
47
- return unitCache.get(cacheKey);
48
- }
49
-
50
- let result;
51
- switch (unit) {
52
- case '%':
53
- result = (pixels / element.parentElement.getBoundingClientRect().width) * 100;
54
- break;
55
- case 'rem':
56
- const remSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
57
- result = pixels / remSize;
58
- break;
59
- case 'em':
60
- const emSize = parseFloat(getComputedStyle(element).fontSize);
61
- result = pixels / emSize;
62
- break;
63
- default:
64
- result = pixels;
65
- }
66
-
67
- unitCache.set(cacheKey, result);
68
- return result;
69
- };
70
-
71
- Alpine.directive('resize', (el, { modifiers, expression }, { evaluate }) => {
72
- // Store configuration on the element for lazy initialization
73
- el._resizeConfig = {
74
- expression,
75
- evaluate,
76
- handles: null,
77
- initialized: false
78
- };
79
-
80
- // Load saved width and height immediately if specified
81
- if (expression) {
82
- try {
83
- const options = evaluate(expression);
84
- if (options) {
85
- if (options.saveWidth) {
86
- const savedWidth = localStorage.getItem(`resizable-${options.saveWidth}`);
87
- if (savedWidth) {
88
- // Preserve the original unit if saved
89
- const [value, unit] = savedWidth.split('|');
90
- el.style.width = `${value}${unit || 'px'}`;
91
- }
92
- }
93
- if (options.saveHeight) {
94
- const savedHeight = localStorage.getItem(`resizable-${options.saveHeight}`);
95
- if (savedHeight) {
96
- // Preserve the original unit if saved
97
- const [value, unit] = savedHeight.split('|');
98
- el.style.height = `${value}${unit || 'px'}`;
99
- }
100
- }
101
- }
102
- } catch (error) {
103
- // Ignore parsing errors here, they'll be handled in initializeResizeElement
104
- }
105
- }
106
-
107
- // Add hover listener to create handles on first interaction
108
- el.addEventListener('mouseenter', initializeResizeElement, { once: true });
109
- });
110
-
111
- function initializeResizeElement(event) {
112
- const el = event.target;
113
- const config = el._resizeConfig;
114
- if (config.initialized) return;
115
-
116
- config.initialized = true;
117
-
118
- // Helper to parse value and unit from CSS dimension
119
- const parseDimension = (value) => {
120
- if (typeof value === 'number') return { value, unit: 'px' };
121
- const match = String(value).match(/^([\d.]+)(.*)$/);
122
- return match ? { value: parseFloat(match[1]), unit: match[2] || 'px' } : { value: 0, unit: 'px' };
123
- };
124
-
125
- // Parse options from expression or use defaults
126
- let options = {};
127
- if (config.expression) {
128
- try {
129
- options = config.evaluate(config.expression);
130
- } catch (error) {
131
- console.error('Error parsing x-resize expression:', config.expression, error);
132
- options = {};
133
- }
134
- }
135
- const {
136
- snapDistance = 0,
137
- snapPoints = [],
138
- snapCloseX = null,
139
- snapDistanceX = null,
140
- snapDistanceY = null,
141
- snapPointsX = [],
142
- snapPointsY = [],
143
- snapCloseY = null,
144
- toggle = null,
145
- handles = ['top', 'bottom', 'left', 'right', 'top-left', 'top-right', 'bottom-left', 'bottom-right'],
146
- saveWidth = null,
147
- saveHeight = null
148
- } = options;
149
-
150
- // Store handles for cleanup
151
- config.handles = [];
152
-
153
- // Parse constraints with units
154
- const constraints = {
155
- closeX: snapCloseX ? parseDimension(snapCloseX) : null,
156
- closeY: snapCloseY ? parseDimension(snapCloseY) : null
157
- };
158
-
159
- // Parse snap points with units
160
- const parsedSnapPoints = snapPoints.map(point => parseDimension(point));
161
- const parsedSnapPointsX = snapPointsX.map(point => parseDimension(point));
162
- const parsedSnapPointsY = snapPointsY.map(point => parseDimension(point));
163
-
164
- // Detect RTL context
165
- const isRTL = getComputedStyle(el).direction === 'rtl';
166
-
167
- // Handle mapping for resize behavior
168
- const handleMap = {
169
- // Physical directions (fixed)
170
- top: { edge: 'top', direction: 'vertical' },
171
- bottom: { edge: 'bottom', direction: 'vertical' },
172
- left: { edge: 'left', direction: 'horizontal' },
173
- right: { edge: 'right', direction: 'horizontal' },
174
-
175
- // Corners
176
- 'top-left': { edge: 'top-left', direction: 'both', edges: ['top', 'left'] },
177
- 'top-right': { edge: 'top-right', direction: 'both', edges: ['top', 'right'] },
178
- 'bottom-left': { edge: 'bottom-left', direction: 'both', edges: ['bottom', 'left'] },
179
- 'bottom-right': { edge: 'bottom-right', direction: 'both', edges: ['bottom', 'right'] },
180
-
181
- // Logical directions (RTL-aware)
182
- start: {
183
- edge: isRTL ? 'right' : 'left',
184
- direction: 'horizontal',
185
- logical: true
186
- },
187
- end: {
188
- edge: isRTL ? 'left' : 'right',
189
- direction: 'horizontal',
190
- logical: true
191
- },
192
-
193
- // Logical corners
194
- 'top-start': {
195
- edge: isRTL ? 'top-right' : 'top-left',
196
- direction: 'both',
197
- edges: isRTL ? ['top', 'right'] : ['top', 'left'],
198
- logical: true
199
- },
200
- 'top-end': {
201
- edge: isRTL ? 'top-left' : 'top-right',
202
- direction: 'both',
203
- edges: isRTL ? ['top', 'left'] : ['top', 'right'],
204
- logical: true
205
- },
206
- 'bottom-start': {
207
- edge: isRTL ? 'bottom-right' : 'bottom-left',
208
- direction: 'both',
209
- edges: isRTL ? ['bottom', 'right'] : ['bottom', 'left'],
210
- logical: true
211
- },
212
- 'bottom-end': {
213
- edge: isRTL ? 'bottom-left' : 'bottom-right',
214
- direction: 'both',
215
- edges: isRTL ? ['bottom', 'left'] : ['bottom', 'right'],
216
- logical: true
217
- }
218
- };
219
-
220
- // Create handles for each specified handle
221
- handles.forEach(handleName => {
222
- const handleInfo = handleMap[handleName];
223
- if (!handleInfo) return;
224
-
225
- const handle = document.createElement('span');
226
- handle.className = `resize-handle resize-handle-${handleName}`;
227
- handle.setAttribute('data-handle', handleName);
228
-
229
- let startX, startY, startWidth, startHeight;
230
- let currentSnap = null;
231
- let lastWidth, lastHeight;
232
-
233
- // Convert constraints to pixels for calculations
234
- const pixelConstraints = {
235
- closeX: constraints.closeX ? convertToPixels(constraints.closeX.value, constraints.closeX.unit) : null,
236
- closeY: constraints.closeY ? convertToPixels(constraints.closeY.value, constraints.closeY.unit) : null
237
- };
238
-
239
- // Convert snap points to pixels
240
- const pixelSnapPoints = parsedSnapPoints.map(point => ({
241
- value: convertToPixels(point.value, point.unit),
242
- unit: point.unit
243
- }));
244
- const pixelSnapPointsX = parsedSnapPointsX.map(point => ({
245
- value: convertToPixels(point.value, point.unit),
246
- unit: point.unit
247
- }));
248
- const pixelSnapPointsY = parsedSnapPointsY.map(point => ({
249
- value: convertToPixels(point.value, point.unit),
250
- unit: point.unit
251
- }));
252
-
253
- const snapDistancePixels = convertToPixels(snapDistance, 'px');
254
- const snapDistanceXPixels = snapDistanceX ? convertToPixels(snapDistanceX, 'px') : snapDistancePixels;
255
- const snapDistanceYPixels = snapDistanceY ? convertToPixels(snapDistanceY, 'px') : snapDistancePixels;
256
-
257
- const handleMouseDown = (e) => {
258
- e.preventDefault();
259
- e.stopPropagation();
260
-
261
- el.classList.add('resizable-dragging');
262
-
263
- startX = e.clientX;
264
- startY = e.clientY;
265
- startWidth = el.getBoundingClientRect().width;
266
- startHeight = el.getBoundingClientRect().height;
267
-
268
- // Each drag starts with clean snap state
269
- currentSnap = null;
270
- lastWidth = startWidth;
271
- lastHeight = startHeight;
272
-
273
- // Show overlay
274
- const overlay = document.querySelector('.resize-overlay') || createOverlay();
275
- overlay.style.display = 'block';
276
- document.body.appendChild(overlay);
277
-
278
- document.addEventListener('mousemove', handleMouseMove);
279
- document.addEventListener('mouseup', handleMouseUp);
280
- };
281
-
282
- const handleMouseMove = (e) => {
283
- const deltaX = e.clientX - startX;
284
- const deltaY = e.clientY - startY;
285
-
286
- let newWidth = startWidth;
287
- let newHeight = startHeight;
288
-
289
- // Calculate new dimensions based on handle type
290
- if (handleInfo.direction === 'horizontal' || handleInfo.direction === 'both') {
291
- if (handleInfo.edge === 'left' || handleInfo.edge === 'top-left' || handleInfo.edge === 'bottom-left') {
292
- newWidth = startWidth - deltaX;
293
- } else if (handleInfo.edge === 'right' || handleInfo.edge === 'top-right' || handleInfo.edge === 'bottom-right') {
294
- newWidth = startWidth + deltaX;
295
- }
296
- }
297
-
298
- if (handleInfo.direction === 'vertical' || handleInfo.direction === 'both') {
299
- if (handleInfo.edge === 'top' || handleInfo.edge === 'top-left' || handleInfo.edge === 'top-right') {
300
- newHeight = startHeight - deltaY;
301
- } else if (handleInfo.edge === 'bottom' || handleInfo.edge === 'bottom-left' || handleInfo.edge === 'bottom-right') {
302
- newHeight = startHeight + deltaY;
303
- }
304
- }
305
-
306
- // Handle snap-close behavior for width
307
- if (pixelConstraints.closeX !== null && newWidth <= pixelConstraints.closeX) {
308
- // Close only while shrinking toward the close edge
309
- if (newWidth < lastWidth || currentSnap === 'closing') {
310
- el.classList.add('resizable-closing');
311
- currentSnap = 'closing';
312
-
313
- if (toggle) {
314
- config.evaluate(`${toggle} = false`);
315
- }
316
-
317
- el.dispatchEvent(new CustomEvent('resize', {
318
- detail: {
319
- width: newWidth,
320
- height: newHeight,
321
- unit: 'px',
322
- snap: currentSnap,
323
- closing: true
324
- }
325
- }));
326
- }
327
- lastWidth = newWidth;
328
- lastHeight = newHeight;
329
- return; // Exit early to prevent further width calculations
330
- }
331
-
332
- // Handle snap-close behavior for height
333
- if (pixelConstraints.closeY !== null && newHeight <= pixelConstraints.closeY) {
334
- // Close only while shrinking toward the close edge
335
- if (newHeight < lastHeight || currentSnap === 'closing') {
336
- el.classList.add('resizable-closing');
337
- currentSnap = 'closing';
338
-
339
- if (toggle) {
340
- config.evaluate(`${toggle} = false`);
341
- }
342
-
343
- el.dispatchEvent(new CustomEvent('resize', {
344
- detail: {
345
- width: newWidth,
346
- height: newHeight,
347
- unit: 'px',
348
- snap: currentSnap,
349
- closing: true
350
- }
351
- }));
352
- }
353
- lastWidth = newWidth;
354
- lastHeight = newHeight;
355
- return; // Exit early to prevent further height calculations
356
- }
357
-
358
- // Above close thresholds: clear any closing state
359
- if (currentSnap === 'closing') {
360
- currentSnap = null;
361
- }
362
-
363
- // Apply constraints only if we're not closing
364
- // Note: maxWidth and maxHeight are now handled by CSS (e.g., Tailwind classes)
365
-
366
- // Handle normal snap points for width
367
- const widthSnapPoints = pixelSnapPointsX.length > 0 ? pixelSnapPointsX : pixelSnapPoints;
368
- const widthSnapDistance = snapDistanceXPixels;
369
- for (const point of widthSnapPoints) {
370
- const distance = Math.abs(newWidth - point.value);
371
- if (distance < widthSnapDistance) {
372
- newWidth = point.value;
373
- currentSnap = `${convertFromPixels(newWidth, point.unit, el)}${point.unit}`;
374
- break;
375
- }
376
- }
377
-
378
- // Handle normal snap points for height
379
- const heightSnapPoints = pixelSnapPointsY.length > 0 ? pixelSnapPointsY : pixelSnapPoints;
380
- const heightSnapDistance = snapDistanceYPixels;
381
- for (const point of heightSnapPoints) {
382
- const distance = Math.abs(newHeight - point.value);
383
- if (distance < heightSnapDistance) {
384
- newHeight = point.value;
385
- currentSnap = `${convertFromPixels(newHeight, point.unit, el)}${point.unit}`;
386
- break;
387
- }
388
- }
389
-
390
- // Convert back to original unit for display
391
- el.style.width = `${newWidth}px`;
392
- el.style.height = `${newHeight}px`;
393
- lastWidth = newWidth;
394
- lastHeight = newHeight;
395
- el.classList.remove('resizable-closing', 'resizable-closed');
396
- if (toggle) {
397
- config.evaluate(`${toggle} = true`);
398
- }
399
-
400
- if (currentSnap !== 'closing') {
401
- if (saveWidth) {
402
- localStorage.setItem(`resizable-${saveWidth}`,
403
- `${newWidth}|px`);
404
- }
405
- if (saveHeight) {
406
- localStorage.setItem(`resizable-${saveHeight}`,
407
- `${newHeight}|px`);
408
- }
409
- }
410
-
411
- // Dispatch resize event
412
- el.dispatchEvent(new CustomEvent('resize', {
413
- detail: {
414
- width: newWidth,
415
- height: newHeight,
416
- unit: 'px',
417
- snap: currentSnap,
418
- closing: currentSnap === 'closing'
419
- }
420
- }));
421
- };
422
-
423
- const handleMouseUp = () => {
424
- document.removeEventListener('mousemove', handleMouseMove);
425
- document.removeEventListener('mouseup', handleMouseUp);
426
-
427
- el.classList.remove('resizable-dragging');
428
-
429
- // Hide overlay
430
- const overlay = document.querySelector('.resize-overlay');
431
- if (overlay) {
432
- overlay.style.display = 'none';
433
- }
434
-
435
- if (currentSnap === 'closing') {
436
- el.classList.add('resizable-closed');
437
- }
438
- };
439
-
440
- handle.addEventListener('mousedown', handleMouseDown);
441
- el.appendChild(handle);
442
- config.handles.push(handle);
443
- });
444
- }
445
-
446
- function createOverlay() {
447
- const overlay = document.createElement('div');
448
- overlay.className = 'resize-overlay';
449
- overlay.style.display = 'none';
450
- return overlay;
451
- }
452
- }
453
-
454
- // Track initialization to prevent duplicates
455
- let resizePluginInitialized = false;
456
-
457
- function ensureResizePluginInitialized() {
458
- if (resizePluginInitialized) return;
459
- if (!window.Alpine || typeof window.Alpine.directive !== 'function') return;
460
-
461
- resizePluginInitialized = true;
462
- initializeResizablePlugin();
463
- }
464
-
465
- // Expose on window for loader to call if needed
466
- window.ensureResizePluginInitialized = ensureResizePluginInitialized;
467
-
468
- // Handle both DOMContentLoaded and alpine:init
469
- if (document.readyState === 'loading') {
470
- document.addEventListener('DOMContentLoaded', ensureResizePluginInitialized);
471
- }
472
-
473
- document.addEventListener('alpine:init', ensureResizePluginInitialized);
474
-
475
- // If Alpine is already initialized when this script loads, initialize immediately
476
- if (window.Alpine && typeof window.Alpine.directive === 'function') {
477
- setTimeout(ensureResizePluginInitialized, 0);
478
- } else {
479
- const checkAlpine = setInterval(() => {
480
- if (window.Alpine && typeof window.Alpine.directive === 'function') {
481
- clearInterval(checkAlpine);
482
- ensureResizePluginInitialized();
483
- }
484
- }, 10);
485
- setTimeout(() => clearInterval(checkAlpine), 5000);
486
- }
487
-
488
- })();