@tldraw/editor 5.3.0-next.299378752aaf → 5.3.0-next.75e2874c4b3f

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.
@@ -183,6 +183,9 @@ export class TextManager extends EditorManager {
183
183
  'font-weight': opts.fontWeight,
184
184
  'font-size': opts.fontSize + 'px',
185
185
  'line-height': `${resolveLineHeightPx(opts.fontSize, opts.lineHeight)}px`,
186
+ // Unitless multiplier consumed by the .tl-rich-text h1–h6 rule (see editor.css),
187
+ // so heading measurement matches on-canvas rendering.
188
+ '--tl-rich-text-heading-line-height': `${opts.lineHeight}`,
186
189
  padding: opts.padding,
187
190
  'max-width': opts.maxWidth ? opts.maxWidth + 'px' : undefined,
188
191
  'min-width': opts.minWidth ? opts.minWidth + 'px' : undefined,
@@ -669,6 +669,30 @@ export abstract class ShapeUtil<Shape extends TLShape = TLShape> {
669
669
  */
670
670
  onBeforeCreate?(next: Shape): Shape | void
671
671
 
672
+ /**
673
+ * A callback called when a shape is reproduced from an existing shape, either by duplicating
674
+ * ({@link Editor.duplicateShapes}) or by pasting/putting content onto the page
675
+ * ({@link Editor.putContentOntoCurrentPage}). This provides a last chance to modify the copy
676
+ * before it's created — for example, to re-stamp attribution so the copy is credited to the
677
+ * current user rather than the original author. It is not called when content is put with
678
+ * `preserveIds` (e.g. {@link Editor.moveShapesToPage}), since the shape keeps its identity
679
+ * and no copy is made.
680
+ *
681
+ * @example
682
+ *
683
+ * ```ts
684
+ * onBeforeDuplicate = (source, duplicate) => {
685
+ * return { ...duplicate, props: { ...duplicate.props, editedBy: this.editor.getAttributionUserId() } }
686
+ * }
687
+ * ```
688
+ *
689
+ * @param source - The shape being copied from.
690
+ * @param duplicate - The new copy (with its own id), before it's created.
691
+ * @returns The next shape or void.
692
+ * @public
693
+ */
694
+ onBeforeDuplicate?(source: Shape, duplicate: Shape): Shape | void
695
+
672
696
  /**
673
697
  * A callback called just before a shape is updated. This method provides a last chance to modify
674
698
  * the updated shape.
@@ -510,6 +510,60 @@ describe('LicenseManager', () => {
510
510
  })
511
511
  })
512
512
 
513
+ describe('Expiry date timezone handling', () => {
514
+ afterEach(() => {
515
+ vi.useRealTimers()
516
+ })
517
+
518
+ // The named expiry date is minted as a date-only string and should mean "the license
519
+ // is fully usable through the end of that day in UTC", giving every user the same
520
+ // cutoff regardless of their local timezone.
521
+ async function makeEvaluationLicenseWithExpiry(expiry: string) {
522
+ const licenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
523
+ licenseInfo[PROPERTIES.FLAGS] = FLAGS.EVALUATION_LICENSE
524
+ licenseInfo[PROPERTIES.EXPIRY_DATE] = expiry
525
+ return generateLicenseKey(JSON.stringify(licenseInfo), keyPair)
526
+ }
527
+
528
+ it('Keeps the license valid through the end of the named expiry day (UTC)', async () => {
529
+ const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
530
+
531
+ // The final moment of the named day is still valid.
532
+ vi.useFakeTimers()
533
+ vi.setSystemTime(new Date('2026-06-26T23:59:59.000Z'))
534
+ expect(
535
+ ((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
536
+ .isEvaluationLicenseExpired
537
+ ).toBe(false)
538
+ })
539
+
540
+ it('Expires the license at the start of the day after the named expiry day (UTC)', async () => {
541
+ const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
542
+
543
+ vi.useFakeTimers()
544
+ vi.setSystemTime(new Date('2026-06-27T00:00:01.000Z'))
545
+ expect(
546
+ ((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
547
+ .isEvaluationLicenseExpired
548
+ ).toBe(true)
549
+ })
550
+
551
+ it('Does not expire early for users west of UTC who are still on the named day locally', async () => {
552
+ const key = await makeEvaluationLicenseWithExpiry('2026-06-26')
553
+
554
+ // Under the old logic a user west of UTC parsed the date one calendar day early and
555
+ // expired at the *start* of that day, losing most of a day. With the uniform UTC
556
+ // cutoff the license stays valid all through June 26 for everyone, no matter their
557
+ // local offset. 1pm UTC on June 26 is well before the cutoff, so it is still valid.
558
+ vi.useFakeTimers()
559
+ vi.setSystemTime(new Date('2026-06-26T13:00:00.000Z'))
560
+ expect(
561
+ ((await licenseManager.getLicenseFromKey(key)) as ValidLicenseKeyResult)
562
+ .isEvaluationLicenseExpired
563
+ ).toBe(false)
564
+ })
565
+ })
566
+
513
567
  describe('License expiry and grace period', () => {
514
568
  it('Fails if the license key has expired beyond grace period', async () => {
515
569
  const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
@@ -540,38 +594,54 @@ describe('LicenseManager', () => {
540
594
  })
541
595
 
542
596
  it('Handles grace period correctly - 20 days expired should still be within grace period', async () => {
543
- const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
544
- const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 20) // 20 days ago
545
- expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString()
546
-
547
- const expiredLicenseKey = await generateLicenseKey(
548
- JSON.stringify(expiredLicenseInfo),
549
- keyPair
550
- )
551
-
552
- // Test the getLicenseFromKey method to verify grace period calculation
553
- const result = (await licenseManager.getLicenseFromKey(
554
- expiredLicenseKey
555
- )) as ValidLicenseKeyResult
556
- expect(result.isAnnualLicense).toBe(true)
557
- expect(result.isAnnualLicenseExpired).toBe(false) // Within 30-day grace period
558
- expect(result.daysSinceExpiry).toBe(20)
597
+ // Pin the clock and use a date-only UTC expiry so the calculation is timezone
598
+ // independent. The named day is fully usable, so an expiry 20 calendar days before
599
+ // "now" has been past its usable window for 19 full days.
600
+ vi.useFakeTimers()
601
+ vi.setSystemTime(new Date('2026-06-26T12:00:00.000Z'))
602
+ try {
603
+ const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
604
+ expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = '2026-06-06' // 20 days before now
605
+
606
+ const expiredLicenseKey = await generateLicenseKey(
607
+ JSON.stringify(expiredLicenseInfo),
608
+ keyPair
609
+ )
610
+
611
+ // Test the getLicenseFromKey method to verify grace period calculation
612
+ const result = (await licenseManager.getLicenseFromKey(
613
+ expiredLicenseKey
614
+ )) as ValidLicenseKeyResult
615
+ expect(result.isAnnualLicense).toBe(true)
616
+ expect(result.isAnnualLicenseExpired).toBe(false) // Within 30-day grace period
617
+ expect(result.daysSinceExpiry).toBe(19)
618
+ } finally {
619
+ vi.useRealTimers()
620
+ }
559
621
  })
560
622
 
561
623
  it('Calculates days since expiry correctly', async () => {
562
- const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
563
- const expiredDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 15) // 15 days ago
564
- expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = expiredDate.toISOString()
565
-
566
- const expiredLicenseKey = await generateLicenseKey(
567
- JSON.stringify(expiredLicenseInfo),
568
- keyPair
569
- )
570
-
571
- const result = (await licenseManager.getLicenseFromKey(
572
- expiredLicenseKey
573
- )) as ValidLicenseKeyResult
574
- expect(result.daysSinceExpiry).toBe(15)
624
+ // Pin the clock and use a date-only UTC expiry so the calculation is timezone
625
+ // independent. The named day is fully usable, so an expiry 15 calendar days before
626
+ // "now" has been past its usable window for 14 full days.
627
+ vi.useFakeTimers()
628
+ vi.setSystemTime(new Date('2026-06-26T12:00:00.000Z'))
629
+ try {
630
+ const expiredLicenseInfo = JSON.parse(STANDARD_LICENSE_INFO)
631
+ expiredLicenseInfo[PROPERTIES.EXPIRY_DATE] = '2026-06-11' // 15 days before now
632
+
633
+ const expiredLicenseKey = await generateLicenseKey(
634
+ JSON.stringify(expiredLicenseInfo),
635
+ keyPair
636
+ )
637
+
638
+ const result = (await licenseManager.getLicenseFromKey(
639
+ expiredLicenseKey
640
+ )) as ValidLicenseKeyResult
641
+ expect(result.daysSinceExpiry).toBe(14)
642
+ } finally {
643
+ vi.useRealTimers()
644
+ }
575
645
  })
576
646
  })
577
647
 
@@ -372,14 +372,26 @@ export class LicenseManager {
372
372
  }
373
373
 
374
374
  private getExpirationDateWithoutGracePeriod(expiryDate: Date) {
375
- return new Date(expiryDate.getFullYear(), expiryDate.getMonth(), expiryDate.getDate())
375
+ // The named expiry date is the last day the license is fully usable, so the license
376
+ // expires at the end of that day, i.e. the start of the following day. We work in UTC
377
+ // (the expiry date is minted and parsed as a UTC date-only string) so the cutoff is a
378
+ // single predictable instant for every user regardless of their local timezone.
379
+ return new Date(
380
+ Date.UTC(
381
+ expiryDate.getUTCFullYear(),
382
+ expiryDate.getUTCMonth(),
383
+ expiryDate.getUTCDate() + 1 // Add 1 day so the named date is fully usable
384
+ )
385
+ )
376
386
  }
377
387
 
378
388
  private getExpirationDateWithGracePeriod(expiryDate: Date) {
379
389
  return new Date(
380
- expiryDate.getFullYear(),
381
- expiryDate.getMonth(),
382
- expiryDate.getDate() + GRACE_PERIOD_DAYS + 1 // Add 1 day to include the expiration day
390
+ Date.UTC(
391
+ expiryDate.getUTCFullYear(),
392
+ expiryDate.getUTCMonth(),
393
+ expiryDate.getUTCDate() + GRACE_PERIOD_DAYS + 1 // Add 1 day to include the expiration day
394
+ )
383
395
  )
384
396
  }
385
397
 
package/src/version.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  // This file is automatically generated by internal/scripts/refresh-assets.ts.
2
2
  // Do not edit manually. Or do, I'm a comment, not a cop.
3
3
 
4
- export const version = '5.3.0-next.299378752aaf'
4
+ export const version = '5.3.0-next.75e2874c4b3f'
5
5
  export const publishDates = {
6
6
  major: '2026-05-06T16:28:18.473Z',
7
- minor: '2026-07-13T13:51:04.172Z',
8
- patch: '2026-07-13T13:51:04.172Z',
7
+ minor: '2026-07-24T17:23:32.181Z',
8
+ patch: '2026-07-24T17:23:32.181Z',
9
9
  }