@widgetic/editor 4.0.10 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -0
- package/dist/components/WidgetPreview.svelte +123 -86
- package/dist/components/WidgetPreview.svelte.d.ts +8 -0
- package/dist/components/color-select/ColorSelect.svelte +4 -4
- package/dist/components/editors/CompositionEditorApp.svelte +562 -0
- package/dist/components/editors/CompositionEditorApp.svelte.d.ts +95 -0
- package/dist/components/editors/EmbedEditor.svelte +33 -22
- package/dist/components/editors/EmbedEditor.svelte.d.ts +1 -1
- package/dist/components/editors/PropsEditor.svelte +267 -850
- package/dist/components/editors/PropsEditor.svelte.d.ts +10 -3
- package/dist/components/ic/AngleInputController.svelte +1 -1
- package/dist/components/ic/BrowserInputController.svelte +58 -31
- package/dist/components/ic/ColorPickerInputController.svelte +55 -21
- package/dist/components/ic/DateFieldInputController.svelte +34 -14
- package/dist/components/ic/DatePickerInputController.svelte +37 -21
- package/dist/components/ic/DropdownInputController.svelte +21 -9
- package/dist/components/ic/DropdownInputController.svelte.d.ts +1 -1
- package/dist/components/ic/FixedPositionInputController.svelte +12 -6
- package/dist/components/ic/FontInputController.svelte +100 -76
- package/dist/components/ic/InputControllerHeader.svelte +16 -9
- package/dist/components/ic/NumberControls.svelte +4 -4
- package/dist/components/ic/NumberInputController.svelte +6 -4
- package/dist/components/ic/PositionInputController.svelte +23 -15
- package/dist/components/ic/RangeCalendarInputController.svelte +36 -20
- package/dist/components/ic/RoundedCornersInputController.svelte +39 -26
- package/dist/components/ic/SliderInputController.svelte +7 -2
- package/dist/components/ic/SwitchInputController.svelte +12 -3
- package/dist/components/ic/TagsInputController.svelte +19 -4
- package/dist/components/ic/TextAreaInputController.svelte +79 -51
- package/dist/components/ic/TextAreaPreviewInputController.svelte +27 -72
- package/dist/components/ic/TextInputController.svelte +17 -1
- package/dist/components/ic/TimeInputController.svelte +11 -4
- package/dist/components/ic/VectorInputController.svelte +101 -70
- package/dist/components/ic/shared/ItemGrid.svelte +12 -4
- package/dist/components/ic/shared/RecentItemsSection.svelte +16 -11
- package/dist/components/ic/shared/RecentItemsSection.svelte.d.ts +1 -1
- package/dist/composition/compositionApi.d.ts +95 -0
- package/dist/composition/compositionApi.js +289 -0
- package/dist/composition/compositionEditorController.d.ts +263 -0
- package/dist/composition/compositionEditorController.js +746 -0
- package/dist/composition/compositionHelpers.d.ts +74 -0
- package/dist/composition/compositionHelpers.js +405 -0
- package/dist/i18n/index.d.ts +4 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/labels.d.ts +15 -0
- package/dist/i18n/labels.js +57 -0
- package/dist/i18n/language.d.ts +228 -0
- package/dist/i18n/language.js +34 -0
- package/dist/i18n/languageKit.d.ts +54 -0
- package/dist/i18n/languageKit.js +102 -0
- package/dist/i18n/messages.d.ts +241 -0
- package/dist/i18n/messages.js +253 -0
- package/dist/i18n/translate.d.ts +6 -0
- package/dist/i18n/translate.js +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/mediaLibraryContext.d.ts +10 -0
- package/dist/mediaLibraryContext.js +30 -0
- package/dist/utils/FontManager.d.ts +21 -5
- package/dist/utils/FontManager.js +62 -67
- package/dist/utils/VectorManager.d.ts +0 -1
- package/dist/utils/VectorManager.js +10 -12
- package/dist/utils/dockPlacement.d.ts +12 -0
- package/dist/utils/dockPlacement.js +13 -0
- package/dist/utils/editorStorage.d.ts +23 -0
- package/dist/utils/editorStorage.js +95 -0
- package/dist/utils/fieldDirection.d.ts +13 -0
- package/dist/utils/fieldDirection.js +25 -0
- package/dist/utils/fontUtils.js +5 -3
- package/dist/utils/htmlSafety.d.ts +21 -0
- package/dist/utils/htmlSafety.js +38 -0
- package/dist/utils/itemReorder.d.ts +90 -0
- package/dist/utils/itemReorder.js +354 -0
- package/dist/utils/recentColors.js +4 -2
- package/dist/utils/richTextPreview.d.ts +38 -0
- package/dist/utils/richTextPreview.js +207 -0
- package/dist/utils/scrollActivity.d.ts +14 -0
- package/dist/utils/scrollActivity.js +63 -0
- package/dist/utils/shadowDom.d.ts +67 -0
- package/dist/utils/shadowDom.js +211 -0
- package/dist/utils/vectorUtils.js +5 -3
- package/dist/utils/visibilityCondition.d.ts +40 -0
- package/dist/utils/visibilityCondition.js +535 -0
- package/package.json +7 -6
|
@@ -63,6 +63,9 @@
|
|
|
63
63
|
import FontManager, { mapGoogleVariantIdsToWeightNames } from '../../utils/FontManager';
|
|
64
64
|
|
|
65
65
|
import { shouldSkipFont } from '../../utils/skipFonts';
|
|
66
|
+
import { escapeHtml, cssQuotedString } from '../../utils/htmlSafety';
|
|
67
|
+
import { acquirePortalHost, releasePortalHost, listenForScroll, queryInTreeOf } from '../../utils/shadowDom';
|
|
68
|
+
import { readEditorStorageValue, writeEditorStorageValue } from '../../utils/editorStorage';
|
|
66
69
|
import type { Writable } from 'svelte/store';
|
|
67
70
|
|
|
68
71
|
import { InputControllerHeader } from './index';
|
|
@@ -71,6 +74,11 @@
|
|
|
71
74
|
import { SearchInput } from '@widgetic/design-system/components';
|
|
72
75
|
import CategoryTabs from './shared/CategoryTabs.svelte';
|
|
73
76
|
import { RecentItemsSection, ItemGrid } from './shared';
|
|
77
|
+
import { useEditorLanguage } from '../../i18n/language';
|
|
78
|
+
import { categoryLabel, fontWeightLabel } from '../../i18n/labels';
|
|
79
|
+
|
|
80
|
+
// The words of the editor in the language of the page.
|
|
81
|
+
const { t } = useEditorLanguage();
|
|
74
82
|
|
|
75
83
|
|
|
76
84
|
const FONT_FILTER_KEY = (id: string) => `font-specific-filter-state-${id}`;
|
|
@@ -125,6 +133,8 @@
|
|
|
125
133
|
|
|
126
134
|
// Font loading optimization
|
|
127
135
|
let fontListContainer: HTMLElement;
|
|
136
|
+
// The control's own element: it says which tree (the page or a module's shadow root) to listen for scrolling in
|
|
137
|
+
let controllerElement: HTMLDivElement | undefined;
|
|
128
138
|
let familyTriggerEl: HTMLDivElement | undefined;
|
|
129
139
|
let familyPopoverStyle = '';
|
|
130
140
|
let familyPopoverSide: 'top' | 'bottom' = 'bottom';
|
|
@@ -191,6 +201,15 @@
|
|
|
191
201
|
// Add a timeout to track long-running initial loads
|
|
192
202
|
let initialLoadTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
193
203
|
|
|
204
|
+
/**
|
|
205
|
+
* The sticky header of the font list. It is looked up in the tree the list lives in (inside a module that is a
|
|
206
|
+
* shadow root, which `document.querySelector` cannot see into), and the id is escaped so that one with dots or
|
|
207
|
+
* brackets in it is found too.
|
|
208
|
+
*/
|
|
209
|
+
function findStickyHeader(): Element | null {
|
|
210
|
+
return queryInTreeOf(fontListContainer, `#${CSS.escape(`font-header-${controllerId}`)}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
194
213
|
// Function to update scroll state
|
|
195
214
|
function updateScrollState() {
|
|
196
215
|
if (fontListContainer) {
|
|
@@ -199,7 +218,7 @@
|
|
|
199
218
|
|
|
200
219
|
// If we're transitioning from scrolled to not scrolled, remove transition
|
|
201
220
|
if (wasScrolled && !isScrolled) {
|
|
202
|
-
const stickyHeader =
|
|
221
|
+
const stickyHeader = findStickyHeader();
|
|
203
222
|
if (stickyHeader) {
|
|
204
223
|
stickyHeader.classList.remove('transition-border');
|
|
205
224
|
// Force a reflow to ensure the transition class removal takes effect
|
|
@@ -207,7 +226,7 @@
|
|
|
207
226
|
}
|
|
208
227
|
} else if (!wasScrolled && isScrolled) {
|
|
209
228
|
// Add transition class when starting to scroll
|
|
210
|
-
const stickyHeader =
|
|
229
|
+
const stickyHeader = findStickyHeader();
|
|
211
230
|
if (stickyHeader) {
|
|
212
231
|
stickyHeader.classList.add('transition-border');
|
|
213
232
|
}
|
|
@@ -252,22 +271,20 @@
|
|
|
252
271
|
|
|
253
272
|
let fontPickerReadyAt = 0;
|
|
254
273
|
|
|
274
|
+
/**
|
|
275
|
+
* The layer the font popovers are drawn in: one of their own, above everything else, in the tree this input lives
|
|
276
|
+
* in. On a page that is a node at the end of the body; inside a module it is a node in the module's portal layer,
|
|
277
|
+
* because a popover drawn in the customer's body would lose the module's styles and leave a node behind in the
|
|
278
|
+
* customer's page. The input has to be on the page to tell which tree it is in, so until it is (a short moment,
|
|
279
|
+
* and a popover is never open by then) the popovers go where the design system's portal configuration sends them.
|
|
280
|
+
*/
|
|
281
|
+
let fontPickerPortalHost: HTMLElement | undefined;
|
|
282
|
+
|
|
255
283
|
function ensureFontPickerPortal() {
|
|
256
|
-
if (!browser) return;
|
|
257
|
-
|
|
258
|
-
if (document.getElementById(portalId)) return;
|
|
259
|
-
const host = document.createElement('div');
|
|
260
|
-
host.id = portalId;
|
|
261
|
-
host.style.position = 'relative';
|
|
262
|
-
host.style.zIndex = '100000000';
|
|
263
|
-
document.body.appendChild(host);
|
|
284
|
+
if (!browser || fontPickerPortalHost) return;
|
|
285
|
+
fontPickerPortalHost = acquirePortalHost(familyTriggerEl, 'widgetic-font-picker-portal') ?? undefined;
|
|
264
286
|
}
|
|
265
287
|
|
|
266
|
-
// The font popover looks up this node while it is created. onMount is too
|
|
267
|
-
// late: the child portal mounts first and throws if the host is missing,
|
|
268
|
-
// which takes the whole details panel (including chat) down with it.
|
|
269
|
-
ensureFontPickerPortal();
|
|
270
|
-
|
|
271
288
|
onMount(async () => {
|
|
272
289
|
try {
|
|
273
290
|
if (browser) {
|
|
@@ -282,13 +299,13 @@
|
|
|
282
299
|
}
|
|
283
300
|
|
|
284
301
|
// Load saved filter
|
|
285
|
-
const savedFilter =
|
|
302
|
+
const savedFilter = readEditorStorageValue(FONT_FILTER_KEY(controllerId));
|
|
286
303
|
if (savedFilter) {
|
|
287
304
|
currentFilter = savedFilter as FilterType;
|
|
288
305
|
}
|
|
289
306
|
|
|
290
307
|
// Load saved weight
|
|
291
|
-
const savedWeight =
|
|
308
|
+
const savedWeight = readEditorStorageValue(FONT_WEIGHT_KEY(controllerId));
|
|
292
309
|
if (savedWeight) {
|
|
293
310
|
selectedWeight = savedWeight;
|
|
294
311
|
}
|
|
@@ -790,7 +807,7 @@
|
|
|
790
807
|
|
|
791
808
|
selectedWeight = `${controllerId}-${newWeight}`;
|
|
792
809
|
if (browser) {
|
|
793
|
-
|
|
810
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
794
811
|
}
|
|
795
812
|
|
|
796
813
|
// Load the full font when selected
|
|
@@ -852,7 +869,7 @@
|
|
|
852
869
|
if (!searchTerm && value) {
|
|
853
870
|
lastActiveFilter = currentFilter;
|
|
854
871
|
currentFilter = 'All';
|
|
855
|
-
|
|
872
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), 'All');
|
|
856
873
|
}
|
|
857
874
|
|
|
858
875
|
// Apply filtering with the new search term
|
|
@@ -1064,7 +1081,7 @@
|
|
|
1064
1081
|
currentFilter = filter;
|
|
1065
1082
|
|
|
1066
1083
|
if (browser) {
|
|
1067
|
-
|
|
1084
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), filter);
|
|
1068
1085
|
}
|
|
1069
1086
|
|
|
1070
1087
|
if (fontManagerInstance) {
|
|
@@ -1482,7 +1499,8 @@
|
|
|
1482
1499
|
* @param term Search term
|
|
1483
1500
|
* @returns HTML string with highlighted match
|
|
1484
1501
|
*
|
|
1485
|
-
* Note:
|
|
1502
|
+
* Note: The result goes to {@html}, so every piece of the font name is escaped as it is put in (a name such as
|
|
1503
|
+
* `<img src=x onerror=...>` must show as text, not run in the page the editor is on).
|
|
1486
1504
|
*/
|
|
1487
1505
|
function highlightMatch(name: string, term: string) {
|
|
1488
1506
|
// Check if this font is the selected one
|
|
@@ -1491,8 +1509,8 @@
|
|
|
1491
1509
|
if (!term) {
|
|
1492
1510
|
// If no search term, just return the name with appropriate color
|
|
1493
1511
|
return isSelected
|
|
1494
|
-
? `<span style="color: white">${name}</span>`
|
|
1495
|
-
: `<span style="color: hsl(var(--grey7))">${name}</span>`;
|
|
1512
|
+
? `<span style="color: white">${escapeHtml(name)}</span>`
|
|
1513
|
+
: `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
|
|
1496
1514
|
}
|
|
1497
1515
|
|
|
1498
1516
|
const nameLower = name.toLowerCase();
|
|
@@ -1501,9 +1519,9 @@
|
|
|
1501
1519
|
// If the name contains the search term, highlight that part
|
|
1502
1520
|
if (nameLower.includes(termLower)) {
|
|
1503
1521
|
const index = nameLower.indexOf(termLower);
|
|
1504
|
-
const beforeMatch = name.slice(0, index);
|
|
1505
|
-
const match = name.slice(index, index + termLower.length);
|
|
1506
|
-
const afterMatch = name.slice(index + termLower.length);
|
|
1522
|
+
const beforeMatch = escapeHtml(name.slice(0, index));
|
|
1523
|
+
const match = escapeHtml(name.slice(index, index + termLower.length));
|
|
1524
|
+
const afterMatch = escapeHtml(name.slice(index + termLower.length));
|
|
1507
1525
|
|
|
1508
1526
|
if (isSelected) {
|
|
1509
1527
|
// For selected items: text is white, highlight is grey7
|
|
@@ -1516,8 +1534,8 @@
|
|
|
1516
1534
|
|
|
1517
1535
|
// Default case - just return the name with appropriate color
|
|
1518
1536
|
return isSelected
|
|
1519
|
-
? `<span style="color: white">${name}</span>`
|
|
1520
|
-
: `<span style="color: hsl(var(--grey7))">${name}</span>`;
|
|
1537
|
+
? `<span style="color: white">${escapeHtml(name)}</span>`
|
|
1538
|
+
: `<span style="color: hsl(var(--grey7))">${escapeHtml(name)}</span>`;
|
|
1521
1539
|
}
|
|
1522
1540
|
|
|
1523
1541
|
/**
|
|
@@ -1550,28 +1568,31 @@
|
|
|
1550
1568
|
}
|
|
1551
1569
|
}
|
|
1552
1570
|
|
|
1553
|
-
//
|
|
1571
|
+
// Stops listening for scrolling (the page and, inside a module, the module's own panels: a scroll never leaves a
|
|
1572
|
+
// shadow root, so a listener on the window alone does not hear about those)
|
|
1573
|
+
let stopListeningForScroll: () => void = () => {};
|
|
1574
|
+
|
|
1575
|
+
// Update onMount to add the scroll listener with passive option
|
|
1554
1576
|
onMount(() => {
|
|
1555
1577
|
// Add scroll listener with passive option
|
|
1556
|
-
|
|
1557
|
-
capture: true,
|
|
1558
|
-
passive: true
|
|
1559
|
-
});
|
|
1578
|
+
stopListeningForScroll = listenForScroll(controllerElement, handleGlobalScroll, { passive: true });
|
|
1560
1579
|
window.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1561
1580
|
window.visualViewport?.addEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1562
1581
|
|
|
1563
|
-
//
|
|
1564
|
-
|
|
1565
|
-
|
|
1582
|
+
// (Two touchstart/touchmove listeners that did nothing used to be added to the window here, and could never be
|
|
1583
|
+
// removed: each input left two more on the customer's page every time it was shown. They did nothing, so they are
|
|
1584
|
+
// gone.)
|
|
1566
1585
|
});
|
|
1567
1586
|
|
|
1568
1587
|
// Update onDestroy to remove all listeners
|
|
1569
1588
|
onDestroy(() => {
|
|
1570
|
-
|
|
1589
|
+
stopListeningForScroll();
|
|
1571
1590
|
window.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1572
1591
|
window.visualViewport?.removeEventListener('resize', handleFamilyPopoverViewportChange);
|
|
1573
|
-
|
|
1574
|
-
|
|
1592
|
+
|
|
1593
|
+
// Give the popover layer back (the last input to leave removes it from the page).
|
|
1594
|
+
releasePortalHost(fontPickerPortalHost);
|
|
1595
|
+
fontPickerPortalHost = undefined;
|
|
1575
1596
|
|
|
1576
1597
|
if (fontObserver) {
|
|
1577
1598
|
fontObserver.disconnect();
|
|
@@ -1653,7 +1674,7 @@
|
|
|
1653
1674
|
function handleWeightSelect(weight: string) {
|
|
1654
1675
|
selectedWeight = `${controllerId}-${weight}`;
|
|
1655
1676
|
if (browser) {
|
|
1656
|
-
|
|
1677
|
+
writeEditorStorageValue(FONT_WEIGHT_KEY(controllerId), selectedWeight);
|
|
1657
1678
|
}
|
|
1658
1679
|
const current = coerceFontValue(value);
|
|
1659
1680
|
if (onChange) {
|
|
@@ -1679,7 +1700,8 @@
|
|
|
1679
1700
|
}
|
|
1680
1701
|
|
|
1681
1702
|
// Ensure the selected item is visible
|
|
1682
|
-
|
|
1703
|
+
// (The family goes through CSS.escape: a name with a quote in it must not end the selector.)
|
|
1704
|
+
const selectedElement = fontListContainer?.querySelector(`[data-font="${CSS.escape(filteredFonts[activeSearchIndex].family)}"]`);
|
|
1683
1705
|
selectedElement?.scrollIntoView({ block: 'nearest' });
|
|
1684
1706
|
}
|
|
1685
1707
|
|
|
@@ -1691,14 +1713,16 @@
|
|
|
1691
1713
|
|
|
1692
1714
|
// Functions for RecentItemsSection
|
|
1693
1715
|
function renderFontItem(font: FontItem) {
|
|
1716
|
+
// The result is used as HTML by RecentItemsSection: the family is quoted for CSS and escaped for HTML where it is
|
|
1717
|
+
// a rule, and escaped for HTML where it is the text.
|
|
1694
1718
|
return {
|
|
1695
1719
|
html: `
|
|
1696
1720
|
<div class="flex items-center flex-1 min-w-0">
|
|
1697
1721
|
<span
|
|
1698
1722
|
class="text-[24px] truncate"
|
|
1699
|
-
style="font-family:
|
|
1723
|
+
style="font-family: ${escapeHtml(cssQuotedString(font.family))}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1700
1724
|
>
|
|
1701
|
-
${font.family}
|
|
1725
|
+
${escapeHtml(font.family)}
|
|
1702
1726
|
</span>
|
|
1703
1727
|
</div>
|
|
1704
1728
|
`
|
|
@@ -1727,7 +1751,7 @@
|
|
|
1727
1751
|
</script>
|
|
1728
1752
|
|
|
1729
1753
|
<!-- Dual IC: header zone always on the left (even if label is empty); 185px inputs on the right. -->
|
|
1730
|
-
<div class="font-ic flex flex-col gap-2 w-full min-w-0 widgetic-colors {className}" style={style}>
|
|
1754
|
+
<div bind:this={controllerElement} class="font-ic flex flex-col gap-2 w-full min-w-0 widgetic-colors {className}" style={style}>
|
|
1731
1755
|
<div class="font-family-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
|
|
1732
1756
|
<div class="font-family-label-ct flex-1 min-w-0">
|
|
1733
1757
|
<InputControllerHeader label={label || ''} />
|
|
@@ -1768,7 +1792,7 @@
|
|
|
1768
1792
|
toggleDropdown(controllerId);
|
|
1769
1793
|
}}
|
|
1770
1794
|
>
|
|
1771
|
-
<Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent
|
|
1795
|
+
<Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group"
|
|
1772
1796
|
onmouseenter={() => {
|
|
1773
1797
|
// Preload fonts when hovering over the button
|
|
1774
1798
|
// This reduces the delay when actually clicking to open
|
|
@@ -1778,10 +1802,10 @@
|
|
|
1778
1802
|
}}
|
|
1779
1803
|
>
|
|
1780
1804
|
<span
|
|
1781
|
-
class="truncate flex-1 text-
|
|
1782
|
-
style="font-family:
|
|
1805
|
+
class="truncate flex-1 text-start subtext text-grey7"
|
|
1806
|
+
style="font-family: {cssQuotedString(resolvedFont?.family || 'system-ui')}, sans-serif; font-display: swap;"
|
|
1783
1807
|
>
|
|
1784
|
-
{resolvedFont?.family ||
|
|
1808
|
+
{resolvedFont?.family || $t('editor.font.select')}
|
|
1785
1809
|
</span>
|
|
1786
1810
|
<IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
|
|
1787
1811
|
</Popover.Trigger>
|
|
@@ -1790,18 +1814,18 @@
|
|
|
1790
1814
|
collisionPadding={12}
|
|
1791
1815
|
side={familyPopoverSide}
|
|
1792
1816
|
sideOffset={4}
|
|
1793
|
-
portalProps={{ to:
|
|
1817
|
+
portalProps={{ to: fontPickerPortalHost }}
|
|
1794
1818
|
class="font-input-popover !p-0 widgetic-colors widgetic-radius rounded-medium modal-shadow border border-grey2"
|
|
1795
1819
|
style={familyPopoverStyle}
|
|
1796
1820
|
>
|
|
1797
1821
|
<div class="flex flex-col h-full min-h-0 font-popover-content">
|
|
1798
1822
|
<!-- Title and upload icon -->
|
|
1799
1823
|
<div class="font-popover-header shrink-0 relative pt-6 pb-[11px] px-5">
|
|
1800
|
-
<div class="h5 text-grey7 widgetic-colors">
|
|
1824
|
+
<div class="h5 text-grey7 widgetic-colors">{$t('editor.font.title')}</div>
|
|
1801
1825
|
<button
|
|
1802
1826
|
type="button"
|
|
1803
1827
|
tabindex="-1"
|
|
1804
|
-
class="absolute
|
|
1828
|
+
class="absolute end-6 top-[26px] bg-transparent border-0 cursor-pointer p-0 rounded-full hover:bg-grey3 transition-all duration-300"
|
|
1805
1829
|
onclick={() => {
|
|
1806
1830
|
try {
|
|
1807
1831
|
openFileBrowser();
|
|
@@ -1821,7 +1845,7 @@
|
|
|
1821
1845
|
<!-- Search input -->
|
|
1822
1846
|
<div class="font-popover-search shrink-0 relative px-4">
|
|
1823
1847
|
<SearchInput
|
|
1824
|
-
placeholder=
|
|
1848
|
+
placeholder={$t('editor.font.search')}
|
|
1825
1849
|
bind:value={searchTerm}
|
|
1826
1850
|
onInput={(e: Event) => {
|
|
1827
1851
|
// Use debounced search only for typing
|
|
@@ -1835,7 +1859,7 @@
|
|
|
1835
1859
|
// Restore the previous filter when clearing search
|
|
1836
1860
|
currentFilter = lastActiveFilter;
|
|
1837
1861
|
if (browser) {
|
|
1838
|
-
|
|
1862
|
+
writeEditorStorageValue(FONT_FILTER_KEY(controllerId), lastActiveFilter);
|
|
1839
1863
|
}
|
|
1840
1864
|
|
|
1841
1865
|
// ... rest of your onClear code ...
|
|
@@ -1849,6 +1873,7 @@
|
|
|
1849
1873
|
categories={filterOptions}
|
|
1850
1874
|
activeCategory={currentFilter}
|
|
1851
1875
|
onCategorySelect={handleFilterChange}
|
|
1876
|
+
getCategoryLabel={(category) => categoryLabel(category, $t)}
|
|
1852
1877
|
filterVisible={!searchTerm}
|
|
1853
1878
|
/>
|
|
1854
1879
|
</div>
|
|
@@ -1865,14 +1890,13 @@
|
|
|
1865
1890
|
}}
|
|
1866
1891
|
id={`font-list-${controllerId}`}
|
|
1867
1892
|
role="listbox"
|
|
1868
|
-
aria-label=
|
|
1893
|
+
aria-label={$t('editor.font.list')}
|
|
1869
1894
|
>
|
|
1870
1895
|
<div class="space-y-0">
|
|
1871
1896
|
<!-- Recently used section - only show when not searching -->
|
|
1872
1897
|
{#if recentFonts.length > 0 && !searchTerm}
|
|
1873
1898
|
<RecentItemsSection
|
|
1874
1899
|
recentItems={recentFonts}
|
|
1875
|
-
title="Recently used"
|
|
1876
1900
|
onItemSelect={handleFontSelect}
|
|
1877
1901
|
onItemDelete={handleFontDelete}
|
|
1878
1902
|
isItemSelected={isFontSelected}
|
|
@@ -1893,9 +1917,9 @@
|
|
|
1893
1917
|
<div id="font-header-{controllerId}" class="sticky top-0 -mt-2 bg-white z-10 pt-2 {isScrolled ? 'border-b border-grey2' : 'border-b border-transparent'} transition-border">
|
|
1894
1918
|
<h3 class="subtext text-grey7 pb-2 flex items-center">
|
|
1895
1919
|
{#if isInitialLoading}
|
|
1896
|
-
<div class="w-4 h-4 border-2 border-t-sage-green border-
|
|
1920
|
+
<div class="w-4 h-4 border-2 border-t-sage-green border-e-sage-green border-b-grey2 border-s-grey2 rounded-full animate-spin me-2"></div>
|
|
1897
1921
|
{/if}
|
|
1898
|
-
{searchTerm ? '
|
|
1922
|
+
{searchTerm ? $t('editor.list.searchResults') : $t('editor.list.more')}
|
|
1899
1923
|
</h3>
|
|
1900
1924
|
</div>
|
|
1901
1925
|
{/if}
|
|
@@ -1905,13 +1929,13 @@
|
|
|
1905
1929
|
{#if isInitialLoading}
|
|
1906
1930
|
<!-- Show compact loading indicator while fonts are loading -->
|
|
1907
1931
|
<div class="flex items-center">
|
|
1908
|
-
<div class="text-grey5 subtext
|
|
1909
|
-
<div class="w-4 h-4 border-2 border-t-sage-green border-
|
|
1932
|
+
<div class="text-grey5 subtext me-2">{$t('editor.font.loading')}</div>
|
|
1933
|
+
<div class="w-4 h-4 border-2 border-t-sage-green border-e-sage-green border-b-grey2 border-s-grey2 rounded-full animate-spin"></div>
|
|
1910
1934
|
</div>
|
|
1911
1935
|
{:else}
|
|
1912
1936
|
<!-- Only show empty state when loading is complete and no fonts were found -->
|
|
1913
1937
|
<EmptyState
|
|
1914
|
-
message={searchTerm ? '
|
|
1938
|
+
message={searchTerm ? $t('editor.font.noneFound') : $t('editor.font.noneAvailable')}
|
|
1915
1939
|
height="auto"
|
|
1916
1940
|
/>
|
|
1917
1941
|
{/if}
|
|
@@ -1934,10 +1958,10 @@
|
|
|
1934
1958
|
style="color: {isFontSelected(font) ? '#ffffff' : 'hsl(var(--grey7))'};"
|
|
1935
1959
|
>
|
|
1936
1960
|
<div class="flex items-center flex-1 min-w-0 group">
|
|
1937
|
-
<Check class="
|
|
1961
|
+
<Check class="me-2 h-6 w-6 flex-shrink-0 {isFontSelected(font) ? 'text-white' : 'text-sage-green opacity-0'}"/>
|
|
1938
1962
|
<span
|
|
1939
1963
|
class="text-[24px] truncate {index === activeSearchIndex && searchTerm ? 'font-medium' : ''}"
|
|
1940
|
-
style="font-family:
|
|
1964
|
+
style="font-family: {cssQuotedString(font.family)}, sans-serif; font-display: swap; color: inherit; -webkit-text-fill-color: currentColor;"
|
|
1941
1965
|
>
|
|
1942
1966
|
{@html highlightMatch(font.family, searchTerm)}
|
|
1943
1967
|
</span>
|
|
@@ -1946,7 +1970,7 @@
|
|
|
1946
1970
|
<div
|
|
1947
1971
|
role="button"
|
|
1948
1972
|
tabindex="-1"
|
|
1949
|
-
class="
|
|
1973
|
+
class="ms-auto transition-opacity {resolvedFont?.family === font.family ? 'opacity-0 pointer-events-none' : 'opacity-0 group-hover:opacity-100'}"
|
|
1950
1974
|
onclick={(e) => {
|
|
1951
1975
|
e.stopPropagation();
|
|
1952
1976
|
e.preventDefault();
|
|
@@ -1955,11 +1979,11 @@
|
|
|
1955
1979
|
>
|
|
1956
1980
|
<img
|
|
1957
1981
|
src={icTrash}
|
|
1958
|
-
alt=
|
|
1982
|
+
alt={$t('editor.font.remove')}
|
|
1959
1983
|
class="w-4 h-4 text-grey5 hover:text-error"
|
|
1960
1984
|
draggable="false"
|
|
1961
1985
|
/>
|
|
1962
|
-
<span class="sr-only">
|
|
1986
|
+
<span class="sr-only">{$t('editor.font.remove')}</span>
|
|
1963
1987
|
</div>
|
|
1964
1988
|
{/if}
|
|
1965
1989
|
</div>
|
|
@@ -1978,7 +2002,7 @@
|
|
|
1978
2002
|
|
|
1979
2003
|
<div class="font-weight-row flex items-center justify-between w-full min-w-0 gap-2" style="min-height: 48px;">
|
|
1980
2004
|
<div class="font-weight-label-ct flex-1 min-w-0">
|
|
1981
|
-
<InputControllerHeader label={label ? '
|
|
2005
|
+
<InputControllerHeader label={label ? $t('editor.font.weight') : ''} />
|
|
1982
2006
|
</div>
|
|
1983
2007
|
<div class="font-weight-input-ct h-12 shrink-0 rounded-large border border-grey2 flex items-center bg-grey1 transition-colors duration-200 hover:border-forest-green" style="width: 185px; max-width: 185px; min-width: 185px;">
|
|
1984
2008
|
<Popover.Root
|
|
@@ -1990,15 +2014,15 @@
|
|
|
1990
2014
|
}
|
|
1991
2015
|
}}
|
|
1992
2016
|
>
|
|
1993
|
-
<Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent
|
|
1994
|
-
<span class="truncate flex-1 text-
|
|
1995
|
-
{displayWeight || 'Regular'}
|
|
2017
|
+
<Popover.Trigger tabindex={-1} class="w-full h-full flex items-center border-0 bg-transparent hover:bg-transparent focus:bg-transparent ps-3 pe-[16px] group">
|
|
2018
|
+
<span class="truncate flex-1 text-start subtext text-grey7">
|
|
2019
|
+
{fontWeightLabel(displayWeight || 'Regular', $t)}
|
|
1996
2020
|
</span>
|
|
1997
2021
|
<IconCaretDownFilled class="w-5 h-5 shrink-0 text-grey5 group-hover:text-grey7" />
|
|
1998
2022
|
</Popover.Trigger>
|
|
1999
|
-
<Popover.Content class="font-input-popover p-0 w-[150px]" portalProps={{ to:
|
|
2023
|
+
<Popover.Content class="font-input-popover p-0 w-[150px]" portalProps={{ to: fontPickerPortalHost }}>
|
|
2000
2024
|
<Command.Root>
|
|
2001
|
-
<Command.List>
|
|
2025
|
+
<Command.List aria-label={$t('editor.font.weightList')}>
|
|
2002
2026
|
<Command.Group>
|
|
2003
2027
|
{#each availableWeights as weight}
|
|
2004
2028
|
<Command.Item
|
|
@@ -2007,9 +2031,9 @@
|
|
|
2007
2031
|
class="px-2 py-2"
|
|
2008
2032
|
>
|
|
2009
2033
|
<Check
|
|
2010
|
-
class="
|
|
2034
|
+
class="me-1 h-4 w-4 {displayWeight === weight ? 'opacity-100' : 'opacity-0'}"
|
|
2011
2035
|
/>
|
|
2012
|
-
<span class="truncate">{weight}</span>
|
|
2036
|
+
<span class="truncate">{fontWeightLabel(weight, $t)}</span>
|
|
2013
2037
|
</Command.Item>
|
|
2014
2038
|
{/each}
|
|
2015
2039
|
</Command.Group>
|
|
@@ -2045,7 +2069,7 @@
|
|
|
2045
2069
|
width: 185px !important;
|
|
2046
2070
|
max-width: 185px !important;
|
|
2047
2071
|
min-width: 185px !important;
|
|
2048
|
-
margin-
|
|
2072
|
+
margin-inline-start: auto;
|
|
2049
2073
|
flex-shrink: 0;
|
|
2050
2074
|
position: relative;
|
|
2051
2075
|
}
|
|
@@ -2192,12 +2216,12 @@
|
|
|
2192
2216
|
|
|
2193
2217
|
/* Style for checkmark icon */
|
|
2194
2218
|
:global(.checkmark-icon) {
|
|
2195
|
-
margin-
|
|
2219
|
+
margin-inline-start: 12px !important;
|
|
2196
2220
|
}
|
|
2197
2221
|
|
|
2198
2222
|
/* Style for font item padding */
|
|
2199
2223
|
:global(.font-item) {
|
|
2200
|
-
padding-
|
|
2224
|
+
padding-inline-start: 48px !important;
|
|
2201
2225
|
}
|
|
2202
2226
|
|
|
2203
2227
|
/* Style for font item height */
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
// Svelte imports
|
|
3
3
|
import { onDestroy, onMount } from 'svelte';
|
|
4
4
|
|
|
5
|
+
// Shared listener for "the user is scrolling" (one per tree, not one per header)
|
|
6
|
+
import { onScrollActivity } from '../../utils/scrollActivity';
|
|
7
|
+
|
|
5
8
|
// Shadcn UI Components from design-system
|
|
6
9
|
import { Tooltip, Portal } from '@widgetic/design-system/components';
|
|
7
10
|
// console.log("InputControllerHeader - Tooltip:", Tooltip);
|
|
@@ -9,6 +12,10 @@ import { Tooltip, Portal } from '@widgetic/design-system/components';
|
|
|
9
12
|
// Lucide Icons from design-system
|
|
10
13
|
import { Info } from '@widgetic/design-system/icons';
|
|
11
14
|
|
|
15
|
+
// The way the screen is read: the tip opens at the end of the line, beside the icon
|
|
16
|
+
import { sideAtEnd, useTextDirection } from '@widgetic/design-system/components/textDirection';
|
|
17
|
+
const textDirection = useTextDirection();
|
|
18
|
+
|
|
12
19
|
// Component props
|
|
13
20
|
let className = '';
|
|
14
21
|
export { className as class };
|
|
@@ -58,23 +65,23 @@ function handleMouseLeave() {
|
|
|
58
65
|
clearTimeout(tooltipTimeout);
|
|
59
66
|
}
|
|
60
67
|
|
|
68
|
+
let headerElement: HTMLElement | undefined;
|
|
69
|
+
let stopListeningForScrolling: () => void = () => {};
|
|
70
|
+
|
|
61
71
|
onMount(() => {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
document.addEventListener('touchmove', handleScroll, { capture: true, passive: true });
|
|
72
|
+
// The listeners for scrolling are shared by all the headers in the same tree (the page or the module's shadow
|
|
73
|
+
// root): every header used to add three of its own to the window and the document.
|
|
74
|
+
stopListeningForScrolling = onScrollActivity(headerElement, handleScroll);
|
|
66
75
|
});
|
|
67
76
|
|
|
68
77
|
onDestroy(() => {
|
|
69
|
-
|
|
70
|
-
document.removeEventListener('wheel', handleScroll, { capture: true });
|
|
71
|
-
document.removeEventListener('touchmove', handleScroll, { capture: true });
|
|
78
|
+
stopListeningForScrolling();
|
|
72
79
|
clearTimeout(scrollTimeout);
|
|
73
80
|
clearTimeout(tooltipTimeout);
|
|
74
81
|
});
|
|
75
82
|
</script>
|
|
76
83
|
|
|
77
|
-
<div class="input-controller-header widgetic-colors {className ? className + ' ' : ''}flex group" {style}>
|
|
84
|
+
<div bind:this={headerElement} class="input-controller-header widgetic-colors {className ? className + ' ' : ''}flex group" {style}>
|
|
78
85
|
<div class="header-label-ct flex-1 min-w-0 flex items-center h-full">
|
|
79
86
|
<div class="tooltip-group flex items-center max-w-full" style="gap: 8px;">
|
|
80
87
|
<span class="subtext cursor-default select-none truncate !text-base" style="color: hsl(var(--grey5));">
|
|
@@ -103,7 +110,7 @@ onDestroy(() => {
|
|
|
103
110
|
{#if showHelp && !isScrolling}
|
|
104
111
|
<Portal target="editor-tooltips">
|
|
105
112
|
<Tooltip.Content
|
|
106
|
-
side=
|
|
113
|
+
side={sideAtEnd($textDirection)}
|
|
107
114
|
sideOffset={5}
|
|
108
115
|
align="center"
|
|
109
116
|
alignOffset={-1}
|
|
@@ -232,10 +232,10 @@
|
|
|
232
232
|
oninput={handleInput}
|
|
233
233
|
onblur={handleBlur}
|
|
234
234
|
readonly={isLocked}
|
|
235
|
-
class="w-full h-full border-0 bg-transparent subtext text-grey7 placeholder:text-grey3 focus-visible:ring-0 focus-visible:ring-offset-0
|
|
235
|
+
class="w-full h-full border-0 bg-transparent subtext text-grey7 placeholder:text-grey3 focus-visible:ring-0 focus-visible:ring-offset-0 ps-4 {customInputClass}"
|
|
236
236
|
/>
|
|
237
237
|
{#if suffix}
|
|
238
|
-
<span class="subtext text-grey7 pointer-events-none -
|
|
238
|
+
<span class="subtext text-grey7 pointer-events-none -ms-1">
|
|
239
239
|
{suffix}
|
|
240
240
|
</span>
|
|
241
241
|
{/if}
|
|
@@ -279,10 +279,10 @@
|
|
|
279
279
|
|
|
280
280
|
<style>
|
|
281
281
|
:global([id^="number-input-"]) {
|
|
282
|
-
text-align:
|
|
282
|
+
text-align: start;
|
|
283
283
|
appearance: textfield;
|
|
284
284
|
-moz-appearance: textfield;
|
|
285
|
-
padding-
|
|
285
|
+
padding-inline-end: 0 !important;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
:global([id^="number-input-"]::-webkit-outer-spin-button),
|
|
@@ -203,11 +203,13 @@
|
|
|
203
203
|
* Blur Event Handler
|
|
204
204
|
* Validates and normalizes input when focus is lost
|
|
205
205
|
*/
|
|
206
|
-
function handleBlur() {
|
|
206
|
+
function handleBlur(event: FocusEvent) {
|
|
207
207
|
// console.log('handleBlur:', { value, isEditing, min, max });
|
|
208
208
|
|
|
209
209
|
isEditing = false;
|
|
210
|
-
|
|
210
|
+
// The input that lost the focus. (It used to be looked up by the id "number-input" in the whole document, which is
|
|
211
|
+
// another input when there are several, and nothing inside a module's shadow root.)
|
|
212
|
+
const input = event.currentTarget as HTMLInputElement;
|
|
211
213
|
|
|
212
214
|
// Handle empty or invalid input
|
|
213
215
|
if (input.value === '' || input.value === '-') {
|
|
@@ -371,7 +373,7 @@
|
|
|
371
373
|
<InputControllerHeader {label} helpText={helpText} />
|
|
372
374
|
</div>
|
|
373
375
|
|
|
374
|
-
<div class="absolute
|
|
376
|
+
<div class="absolute end-0">
|
|
375
377
|
<NumberControls
|
|
376
378
|
{value}
|
|
377
379
|
{onChange}
|
|
@@ -391,7 +393,7 @@
|
|
|
391
393
|
|
|
392
394
|
<style>
|
|
393
395
|
:global(#number-input) {
|
|
394
|
-
text-align:
|
|
396
|
+
text-align: start;
|
|
395
397
|
appearance: textfield;
|
|
396
398
|
-moz-appearance: textfield;
|
|
397
399
|
}
|