@brftech/filex-core 0.42.2 → 0.43.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 +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-CC6rJXeP.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-CAoTSXTO.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CdZeK4j4.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5227 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1055 -253
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +31 -15
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -62
- package/src/components/SSHKeysPanel.vue +79 -30
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -31
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +18 -3
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +515 -117
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +536 -152
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2890 -260
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +50 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-BUQWLhwX.js +0 -124
- package/dist/ArchiveViewer-BUQWLhwX.js.map +0 -1
- package/dist/CsvViewer-Bg2_aqzc.js +0 -141
- package/dist/CsvViewer-Bg2_aqzc.js.map +0 -1
- package/dist/DrawioViewer-CC6rJXeP.js.map +0 -1
- package/dist/EpubViewer-CAoTSXTO.js.map +0 -1
- package/dist/IpynbViewer-D0GwGpnE.js +0 -184
- package/dist/IpynbViewer-D0GwGpnE.js.map +0 -1
- package/dist/MermaidViewer-DGwAvRZg.js +0 -137
- package/dist/MermaidViewer-DGwAvRZg.js.map +0 -1
- package/dist/PsdViewer-DhGweA0m.js +0 -122
- package/dist/PsdViewer-DhGweA0m.js.map +0 -1
- package/dist/TiffViewer-D0AurvAh.js +0 -142
- package/dist/TiffViewer-D0AurvAh.js.map +0 -1
- package/dist/Viewer3D-CdZeK4j4.js.map +0 -1
- package/dist/index-BArI-B1M.js +0 -26911
- package/dist/index-BArI-B1M.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
- package/src/composables/useScrolledX.ts +0 -21
package/src/lib/tags.ts
CHANGED
|
@@ -22,13 +22,83 @@
|
|
|
22
22
|
|
|
23
23
|
const TTL_MS = 60_000;
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The two kinds of tag (v0.43.0, the owner's decision "tagler personal ve team
|
|
27
|
+
* olarak ikiye ayrılır"):
|
|
28
|
+
*
|
|
29
|
+
* personal — the person's own label, like a star. Nobody else sees it.
|
|
30
|
+
* team — shared with everyone in the tenant who can see the file;
|
|
31
|
+
* adding or removing one needs edit permission on the file.
|
|
32
|
+
*
|
|
33
|
+
* ⚠ Before v0.43.0 there was one kind and it was shared with EVERY account on
|
|
34
|
+
* the server while the code called it per-user — a tester's "müşteri teklifi"
|
|
35
|
+
* showed in other people's panels and they could remove it. A tag on screen
|
|
36
|
+
* therefore always says which kind it is (`TagKindIcon` + the words), so
|
|
37
|
+
* nobody has to guess who can see it.
|
|
38
|
+
*/
|
|
39
|
+
export type TagKind = 'personal' | 'team';
|
|
40
|
+
|
|
41
|
+
/** A tag as the server hands it out: its name as typed and its kind. */
|
|
42
|
+
export interface TagItem {
|
|
43
|
+
name: string;
|
|
44
|
+
kind: TagKind;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isTagKind(v: unknown): v is TagKind {
|
|
48
|
+
return v === 'personal' || v === 'team';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Whether two names are one tag, the way the server decides it
|
|
53
|
+
* (`internal/tagname.Key`): whitespace collapsed, NFC, the four Latin i's —
|
|
54
|
+
* I, ı, İ, i — as one letter, then case-insensitive. The server is the
|
|
55
|
+
* authority; the client uses this only so the picker does not offer to add a
|
|
56
|
+
* tag the file already carries under different capitals.
|
|
57
|
+
*
|
|
58
|
+
* ⚠ The i's are mapped BEFORE lower-casing: JS lower-cases `İ` to "i" + U+0307 (a
|
|
59
|
+
* combining dot) and leaves `ı` alone, so `toLowerCase` alone would call
|
|
60
|
+
* "IŞIK" and "ışık" two tags — the Turkish case the server folds together.
|
|
61
|
+
*/
|
|
62
|
+
export function tagKey(name: string): string {
|
|
63
|
+
return String(name ?? '')
|
|
64
|
+
.normalize('NFC')
|
|
65
|
+
.trim()
|
|
66
|
+
.replace(/\s+/g, ' ')
|
|
67
|
+
.replace(/[Iıİ]/g, 'i')
|
|
68
|
+
.toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The items in a tag answer. A v0.43+ server sends `items: [{name, kind}]`; an
|
|
73
|
+
* older one sends only `tags: string[]` — and on an older server every tag was
|
|
74
|
+
* shared with everybody, so those are TEAM tags. Anything malformed is dropped.
|
|
75
|
+
*/
|
|
76
|
+
export function tagItemsOf(body: unknown): TagItem[] {
|
|
77
|
+
const b = (body ?? {}) as { items?: unknown; tags?: unknown };
|
|
78
|
+
if (Array.isArray(b.items)) {
|
|
79
|
+
return b.items.filter(
|
|
80
|
+
(x: unknown): x is TagItem =>
|
|
81
|
+
!!x &&
|
|
82
|
+
typeof (x as TagItem).name === 'string' &&
|
|
83
|
+
(x as TagItem).name !== '' &&
|
|
84
|
+
isTagKind((x as TagItem).kind),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (Array.isArray(b.tags)) {
|
|
88
|
+
return b.tags
|
|
89
|
+
.filter((x: unknown): x is string => typeof x === 'string' && x !== '')
|
|
90
|
+
.map((name) => ({ name, kind: 'team' as const }));
|
|
91
|
+
}
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
25
95
|
interface CacheEntry {
|
|
26
96
|
at: number;
|
|
27
|
-
tags:
|
|
97
|
+
tags: TagItem[];
|
|
28
98
|
}
|
|
29
99
|
|
|
30
100
|
const cache = new Map<string, CacheEntry>();
|
|
31
|
-
const inflight = new Map<string, Promise<
|
|
101
|
+
const inflight = new Map<string, Promise<TagItem[]>>();
|
|
32
102
|
|
|
33
103
|
export interface TagListOptions {
|
|
34
104
|
apiBase?: string;
|
|
@@ -45,11 +115,50 @@ export function invalidateTagCache(): void {
|
|
|
45
115
|
}
|
|
46
116
|
|
|
47
117
|
/**
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
118
|
+
* etiket:k2 — "the tags changed", told to every explorer on the page.
|
|
119
|
+
*
|
|
120
|
+
* ⚠⚠ This used to travel as a component EVENT (TagPicker `change` → the
|
|
121
|
+
* explorer's `onNodeTagsChanged`), and Vue drops an event emitted by a
|
|
122
|
+
* component that has already been unmounted. The save is asynchronous, so a
|
|
123
|
+
* person who added a tag and closed the dialog before the server answered —
|
|
124
|
+
* a fraction of a second on a busy server — saved the tag and kept the panel
|
|
125
|
+
* that said "No tags yet". Measured: the full Playwright run (a loaded
|
|
126
|
+
* server) showed exactly that, while the spec alone (an idle one) passed.
|
|
127
|
+
* A module-level announcement does not depend on the component still being
|
|
128
|
+
* there, and it reaches EVERY explorer on the page, not only the one that
|
|
129
|
+
* hosted the picker.
|
|
130
|
+
*/
|
|
131
|
+
const changeListeners = new Set<() => void>();
|
|
132
|
+
|
|
133
|
+
/** Listen for tag writes; returns the function that stops listening. */
|
|
134
|
+
export function onTagsChanged(fn: () => void): () => void {
|
|
135
|
+
changeListeners.add(fn);
|
|
136
|
+
return () => {
|
|
137
|
+
changeListeners.delete(fn);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** A tag write succeeded: drop the cache, then tell every listener. The
|
|
142
|
+
* listeners re-ask WITHOUT forcing, so N explorers still cost one request
|
|
143
|
+
* (the first creates it, the rest join it in flight). */
|
|
144
|
+
export function announceTagsChanged(): void {
|
|
145
|
+
invalidateTagCache();
|
|
146
|
+
for (const fn of [...changeListeners]) {
|
|
147
|
+
try {
|
|
148
|
+
fn();
|
|
149
|
+
} catch {
|
|
150
|
+
/* one listener's failure must not silence the others */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Every tag the person can see — both kinds — alphabetical, as the backend
|
|
157
|
+
* returns them. Never throws: an older backend with no such route, or a
|
|
158
|
+
* caller with no permission, gets an empty list and the panel simply shows
|
|
159
|
+
* no tag section.
|
|
51
160
|
*/
|
|
52
|
-
export async function fetchAllTags(opts: TagListOptions = {}): Promise<
|
|
161
|
+
export async function fetchAllTags(opts: TagListOptions = {}): Promise<TagItem[]> {
|
|
53
162
|
const base = opts.apiBase ?? '';
|
|
54
163
|
const key = base || '(same-origin)';
|
|
55
164
|
if (!opts.force) {
|
|
@@ -65,10 +174,7 @@ export async function fetchAllTags(opts: TagListOptions = {}): Promise<string[]>
|
|
|
65
174
|
credentials: opts.authCredentials ?? 'same-origin',
|
|
66
175
|
});
|
|
67
176
|
if (!res.ok) return [];
|
|
68
|
-
const
|
|
69
|
-
const tags: string[] = Array.isArray(body?.tags)
|
|
70
|
-
? body.tags.filter((x: unknown): x is string => typeof x === 'string' && x !== '')
|
|
71
|
-
: [];
|
|
177
|
+
const tags = tagItemsOf(await res.json());
|
|
72
178
|
cache.set(key, { at: Date.now(), tags });
|
|
73
179
|
return tags;
|
|
74
180
|
} catch {
|
|
@@ -84,16 +190,19 @@ export async function fetchAllTags(opts: TagListOptions = {}): Promise<string[]>
|
|
|
84
190
|
/**
|
|
85
191
|
* Nodes carrying `tag`, as raw node rows (`{id, path, name, type, …}`) — the
|
|
86
192
|
* same shape `star/list` and `recent` answer with, so the caller maps them
|
|
87
|
-
* through its own `nodeRowToFileNode`.
|
|
193
|
+
* through its own `nodeRowToFileNode`. `kind` narrows to one kind; '' = both
|
|
194
|
+
* (what a `#.tag~x` link from before v0.43 means).
|
|
88
195
|
*/
|
|
89
196
|
export async function fetchTaggedRows(
|
|
90
197
|
tag: string,
|
|
91
198
|
opts: TagListOptions = {},
|
|
92
199
|
limit = 200,
|
|
200
|
+
kind: TagKind | '' = '',
|
|
93
201
|
): Promise<Record<string, unknown>[]> {
|
|
94
202
|
const base = opts.apiBase ?? '';
|
|
203
|
+
const k = kind ? `&kind=${kind}` : '';
|
|
95
204
|
const res = await fetch(
|
|
96
|
-
`${base}/api/files/manager/tagged?tag=${encodeURIComponent(tag)}&limit=${limit}`,
|
|
205
|
+
`${base}/api/files/manager/tagged?tag=${encodeURIComponent(tag)}&limit=${limit}${k}`,
|
|
97
206
|
{
|
|
98
207
|
headers: await (opts.authHeaders ?? (() => ({})))(),
|
|
99
208
|
credentials: opts.authCredentials ?? 'same-origin',
|
package/src/lib/themes.ts
CHANGED
|
@@ -45,21 +45,43 @@
|
|
|
45
45
|
*/
|
|
46
46
|
|
|
47
47
|
import { ref, type Ref } from 'vue';
|
|
48
|
+
import { hasSession, registerPersonalMirror, savePref, setLocalPref } from './prefs';
|
|
48
49
|
|
|
49
50
|
/** Map of `--fe-*` custom property → value. */
|
|
50
51
|
export type ThemeTokenMap = Record<string, string>;
|
|
51
52
|
|
|
52
53
|
export interface ThemeDef {
|
|
53
|
-
/** Stable id —
|
|
54
|
+
/** Stable id — what a palette choice stores (on the account where the host
|
|
55
|
+
* keeps preferences there, `lib/prefs`; mirrored in localStorage for the
|
|
56
|
+
* first paint), never shown to users. */
|
|
54
57
|
id: string;
|
|
55
|
-
/**
|
|
56
|
-
|
|
58
|
+
/**
|
|
59
|
+
* i18n catalogue key for the display name (tr + en).
|
|
60
|
+
*
|
|
61
|
+
* ⚠ Built-in palettes only. An operator's own theme carries `name` instead:
|
|
62
|
+
* "Acme Bulut" is not a string this product translates, and putting it
|
|
63
|
+
* through `t()` would print the key straight back at them the first time
|
|
64
|
+
* somebody named a theme something the catalogue did not contain.
|
|
65
|
+
*/
|
|
66
|
+
nameKey?: string;
|
|
67
|
+
/** Literal display name — operator-defined themes only (tema:v1). */
|
|
68
|
+
name?: string;
|
|
57
69
|
/** Palette applied when the resolved mode is light. */
|
|
58
70
|
light: ThemeTokenMap;
|
|
59
71
|
/** Palette applied when the resolved mode is dark. */
|
|
60
72
|
dark: ThemeTokenMap;
|
|
61
73
|
}
|
|
62
74
|
|
|
75
|
+
/**
|
|
76
|
+
* tema:v1 — the prefix that keeps the two namespaces apart.
|
|
77
|
+
*
|
|
78
|
+
* An operator-defined theme's id is always `custom:<slug>`, so no name an
|
|
79
|
+
* operator types can ever shadow `night`, `forest` or `default`. The server
|
|
80
|
+
* stores the slug bare and adds the prefix on the way out
|
|
81
|
+
* (backend/internal/api/handlers/themes.go).
|
|
82
|
+
*/
|
|
83
|
+
export const CUSTOM_THEME_PREFIX = 'custom:';
|
|
84
|
+
|
|
63
85
|
/**
|
|
64
86
|
* ⚠⚠ `filex.palette`, NOT `filex.theme` — and the rename is a BUG FIX, not
|
|
65
87
|
* tidying.
|
|
@@ -476,25 +498,135 @@ export const THEMES: ThemeDef[] = [
|
|
|
476
498
|
},
|
|
477
499
|
];
|
|
478
500
|
|
|
501
|
+
/* ------------------------------------------------------------------ */
|
|
502
|
+
/* tema:v1 — operator-defined themes */
|
|
503
|
+
/* ------------------------------------------------------------------ */
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* The instance's own themes, fetched once per page load from the public
|
|
507
|
+
* `GET /api/appearance` and published here.
|
|
508
|
+
*
|
|
509
|
+
* ⚠⚠ THE SERVER'S LIST IS THE ONLY DEFINITION OF "EXISTS", and that is what
|
|
510
|
+
* makes deleting a theme safe. Nothing goes and rewrites the stored choice of
|
|
511
|
+
* everybody who was using a deleted theme — there is more than one place such
|
|
512
|
+
* a choice can live (this browser's localStorage, a per-account document on
|
|
513
|
+
* the server) and a cleanup pass that missed one would leave somebody pinned
|
|
514
|
+
* to a palette that no longer paints anything. Instead every lookup RESOLVES:
|
|
515
|
+
* `themeById` returns undefined for an id the server did not send, and every
|
|
516
|
+
* caller already treats undefined as "the stock palette".
|
|
517
|
+
*/
|
|
518
|
+
const customThemes: Ref<ThemeDef[]> = ref([]);
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Replace the operator-defined themes. Called once at boot with the payload
|
|
522
|
+
* from `/api/appearance`.
|
|
523
|
+
*
|
|
524
|
+
* ⚠ It also RE-RESOLVES the active selection, which is the client half of
|
|
525
|
+
* "deleting a theme puts anybody using it back on the default": a browser
|
|
526
|
+
* holding `custom:acme` in localStorage keeps it across the fetch, and the
|
|
527
|
+
* moment a list arrives that does not contain it, the selection drops to
|
|
528
|
+
* `default`.
|
|
529
|
+
*
|
|
530
|
+
* ⚠⚠ The correction is LOCAL — `applyStoredPalette`, never `setTheme`. Third
|
|
531
|
+
* merge repair, and the most destructive of the three had it been left.
|
|
532
|
+
* `setTheme` now writes the account, so correcting through it would send a
|
|
533
|
+
* fact the SERVER just told us straight back to the server, as though the
|
|
534
|
+
* person had chosen it. That is harmless only when the correction is right.
|
|
535
|
+
* It is not always right: this browser's mirror can hold a stale id from
|
|
536
|
+
* before the account was read, the two fetches have no ordering, and a
|
|
537
|
+
* correction fired from that stale value would overwrite the account's real
|
|
538
|
+
* choice — on every device — for a theme that was never deleted.
|
|
539
|
+
*
|
|
540
|
+
* Leaving the account's dead id untouched costs nothing: every read resolves
|
|
541
|
+
* it to the stock palette anyway, which is the whole point of building
|
|
542
|
+
* deletion as resolution rather than as a cleanup pass. The first deliberate
|
|
543
|
+
* pick the person makes writes a live id over it.
|
|
544
|
+
*/
|
|
545
|
+
export function setCustomThemes(defs: ThemeDef[]): void {
|
|
546
|
+
customThemes.value = Array.isArray(defs) ? defs : [];
|
|
547
|
+
if (themeId.value !== DEFAULT_THEME_ID && !themeById(themeId.value)) {
|
|
548
|
+
applyStoredPalette(DEFAULT_THEME_ID);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Reactive handle for components that render the palette list. */
|
|
553
|
+
export function useCustomThemes(): { customThemes: Ref<ThemeDef[]> } {
|
|
554
|
+
return { customThemes };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Every palette a person may pick: the built-ins, then the instance's own.
|
|
559
|
+
*
|
|
560
|
+
* ⚠ Operator themes come LAST and are never interleaved. The built-in order
|
|
561
|
+
* is the order of the gallery cards people have learned, and a theme named
|
|
562
|
+
* "Amber Kurumsal" sorting itself between Amber and Lilac would move the card
|
|
563
|
+
* under somebody's cursor the first time a theme was added.
|
|
564
|
+
*/
|
|
565
|
+
export function allThemes(): ThemeDef[] {
|
|
566
|
+
return [...THEMES, ...customThemes.value];
|
|
567
|
+
}
|
|
568
|
+
|
|
479
569
|
export function themeById(id: string | null | undefined): ThemeDef | undefined {
|
|
480
|
-
|
|
570
|
+
if (!id) return undefined;
|
|
571
|
+
return THEMES.find((t) => t.id === id) ?? customThemes.value.find((t) => t.id === id);
|
|
481
572
|
}
|
|
482
573
|
|
|
483
|
-
/**
|
|
484
|
-
*
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
574
|
+
/** The display name of a theme, translated for built-ins and literal for an
|
|
575
|
+
* operator's own. */
|
|
576
|
+
export function themeName(theme: ThemeDef, t: (key: string) => string): string {
|
|
577
|
+
return theme.name ?? (theme.nameKey ? t(theme.nameKey) : theme.id);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Every token key ANY theme touches — used to fully clear inline overrides
|
|
582
|
+
* when switching themes (a theme that skips a token must not inherit the
|
|
583
|
+
* previous theme's value for it).
|
|
584
|
+
*
|
|
585
|
+
* ⚠⚠ A FUNCTION, not the constant it used to be. Computed once at module load
|
|
586
|
+
* it covered only the built-in palettes, so switching FROM an operator theme
|
|
587
|
+
* that set a token no built-in declares would leave that token's inline value
|
|
588
|
+
* on the root element — the new palette applied, and one stray colour from the
|
|
589
|
+
* old one survived it. It has to be recomputed against the list that is
|
|
590
|
+
* actually installed.
|
|
591
|
+
*/
|
|
592
|
+
function allTokenKeys(): string[] {
|
|
593
|
+
return Array.from(
|
|
594
|
+
new Set(allThemes().flatMap((t) => [...Object.keys(t.light), ...Object.keys(t.dark)])),
|
|
595
|
+
);
|
|
596
|
+
}
|
|
489
597
|
|
|
490
598
|
/* ------------------------------------------------------------------ */
|
|
491
599
|
/* Shared reactive state + persistence */
|
|
492
600
|
/* ------------------------------------------------------------------ */
|
|
493
601
|
|
|
602
|
+
/**
|
|
603
|
+
* Whether an id is worth holding on to before the server's theme list has
|
|
604
|
+
* arrived.
|
|
605
|
+
*
|
|
606
|
+
* ⚠ `themeById` cannot answer this at module load: the operator themes are
|
|
607
|
+
* fetched over the network and the first paint happens long before they land.
|
|
608
|
+
* A boot check that rejected every `custom:` id would silently reset the
|
|
609
|
+
* palette of anybody whose choice was an operator theme, on every page load —
|
|
610
|
+
* the preference would look like it never saved. So a prefixed id is kept on
|
|
611
|
+
* trust here and re-resolved for real by `setCustomThemes` once the list is
|
|
612
|
+
* in. Until then nothing paints it, which is the stock palette: correct, and
|
|
613
|
+
* the same thing the person would see anyway while the fetch is in flight.
|
|
614
|
+
*/
|
|
615
|
+
function plausibleThemeId(v: string | null): boolean {
|
|
616
|
+
return !!v && (!!themeById(v) || v.startsWith(CUSTOM_THEME_PREFIX));
|
|
617
|
+
}
|
|
618
|
+
|
|
494
619
|
function readStoredThemeId(): string {
|
|
620
|
+
// ⚠⚠ THE MIRROR BELONGS TO A PERSON, so with nobody signed in it is not read
|
|
621
|
+
// at all (`lib/prefs` → `hasSession`). It is not a machine's palette: it is a
|
|
622
|
+
// cache of ONE ACCOUNT's answer, and handing it to a signed-out window paints
|
|
623
|
+
// the last person who used this browser onto the sign-in page — which is what
|
|
624
|
+
// this line was measured doing on 2026-09-21, and is also how the operator's
|
|
625
|
+
// own default came to be suppressed (`instanceThemes.hasOwnChoice`).
|
|
626
|
+
if (!hasSession()) return DEFAULT_THEME_ID;
|
|
495
627
|
try {
|
|
496
628
|
const v = localStorage.getItem(THEME_LS_KEY);
|
|
497
|
-
if (
|
|
629
|
+
if (plausibleThemeId(v)) return v as string;
|
|
498
630
|
// One-time carry-over from the shared key. `themeById` is what makes this
|
|
499
631
|
// safe: 'light' / 'dark' / 'auto' are not palette ids, so a value the app's
|
|
500
632
|
// mode wrote can never be mistaken for a palette here.
|
|
@@ -516,15 +648,105 @@ const themeId: Ref<string> = ref(
|
|
|
516
648
|
typeof window === 'undefined' ? DEFAULT_THEME_ID : readStoredThemeId(),
|
|
517
649
|
);
|
|
518
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Pick a palette.
|
|
653
|
+
*
|
|
654
|
+
* ⚠⚠ It is saved on the ACCOUNT, not only in this browser (`lib/prefs`).
|
|
655
|
+
* A palette lived in `localStorage` alone until v3, which meant a person who
|
|
656
|
+
* chose one on their laptop opened the product on their phone in the stock
|
|
657
|
+
* colours and had to choose again — reported by Burak as exactly that. The
|
|
658
|
+
* localStorage write stays as the FIRST-PAINT cache: it is what this browser
|
|
659
|
+
* reads before the account's answer can arrive, so the window does not flash
|
|
660
|
+
* the default on the way to the chosen one.
|
|
661
|
+
*/
|
|
519
662
|
export function setTheme(id: string): void {
|
|
520
|
-
const valid =
|
|
663
|
+
const valid = plausibleThemeId(id) ? id : DEFAULT_THEME_ID;
|
|
521
664
|
themeId.value = valid;
|
|
522
665
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
666
|
+
// ⚠ tema:v1 — the stock palette is WRITTEN, not cleared. It used to be
|
|
667
|
+
// removed, on the reasonable-sounding grounds that "no key" and "the
|
|
668
|
+
// default" mean the same thing. They stopped meaning the same thing the
|
|
669
|
+
// moment an instance could have a default of its own: an operator whose
|
|
670
|
+
// house style is a brand theme needs to tell "this person has not chosen"
|
|
671
|
+
// from "this person deliberately chose the product's own colours", and
|
|
672
|
+
// with the key cleared those are the same state. The second one would
|
|
673
|
+
// have had the brand theme re-applied over it on every page load, which
|
|
674
|
+
// reads as a preference that will not save.
|
|
675
|
+
localStorage.setItem(THEME_LS_KEY, valid);
|
|
525
676
|
} catch {
|
|
526
677
|
/* quota / private mode */
|
|
527
678
|
}
|
|
679
|
+
// ⚠⚠ `valid`, NEVER ''. This line is a merge repair and it must not go back.
|
|
680
|
+
// `THEME_LS_KEY` and `PREF_LS_KEYS.palette` are the SAME string,
|
|
681
|
+
// 'filex.palette'; `savePref` mirrors into local storage and `setLocalPref`
|
|
682
|
+
// DELETES the key when handed an empty value. So a `'' for stock` ternary
|
|
683
|
+
// here wrote the key on the line above and erased it on this one:
|
|
684
|
+
// `setTheme('default')` left no trace of itself, the account learned
|
|
685
|
+
// nothing, and "deliberately chose the product's own colours" collapsed back
|
|
686
|
+
// into "has never chosen" — the very state an instance default paints over,
|
|
687
|
+
// on every page load. "Never chosen" is the key being ABSENT, and nothing
|
|
688
|
+
// else.
|
|
689
|
+
savePref('palette', valid);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The account's palette has arrived: paint it, without sending it back.
|
|
694
|
+
*
|
|
695
|
+
* ⚠ Separate from `setTheme` on purpose. Echoing a value that came FROM the
|
|
696
|
+
* server back TO it is how a hydration turns into a write, and a write into
|
|
697
|
+
* another device's surprise.
|
|
698
|
+
*
|
|
699
|
+
* ⚠⚠ `plausibleThemeId`, not `themeById` — the second merge repair, and the
|
|
700
|
+
* race it closes is worth spelling out. The account's answer and the operator
|
|
701
|
+
* themes are two independent fetches with no ordering between them. When the
|
|
702
|
+
* account arrives first and says `custom:acme`, `themeById` cannot know that
|
|
703
|
+
* id yet (the list is still in flight), would call it invalid, and would drop
|
|
704
|
+
* the person onto the stock palette — writing that demotion into this
|
|
705
|
+
* browser's mirror. `setCustomThemes` then finds the selection already at the
|
|
706
|
+
* default and sees nothing to correct, so the choice is gone rather than
|
|
707
|
+
* restored. Holding a prefixed id on trust and letting the list resolve it for
|
|
708
|
+
* real is the same contract `readStoredThemeId` already follows at boot.
|
|
709
|
+
*/
|
|
710
|
+
export function applyStoredPalette(id: string | undefined | null): void {
|
|
711
|
+
// ⚠⚠ NOTHING is not an ANSWER. An account that carries no `palette` key has
|
|
712
|
+
// never been asked the question on this server; it has not said "the stock
|
|
713
|
+
// palette". Treating the two the same is how e2e 98-custom-theme.spec.ts:97
|
|
714
|
+
// went red: the browser painted its own `custom:e2e-acme` at boot, then
|
|
715
|
+
// `App.vue` hydrated an account document with no palette in it and called
|
|
716
|
+
// this function with `''`, which resolved to the stock id and overwrote both
|
|
717
|
+
// the selection AND this browser's mirror. `expect.poll(--fe-primary)` won
|
|
718
|
+
// the race against the fetch and the unpolled `--fe-bg` read on the very
|
|
719
|
+
// next line lost it, which is exactly what a flake caused by a real bug
|
|
720
|
+
// looks like.
|
|
721
|
+
//
|
|
722
|
+
// ⚠ The other three preferences already got this right and only the palette
|
|
723
|
+
// did not — `applyAccountTheme` (web/src/lib/theme.ts), `applyAccountDensity`
|
|
724
|
+
// (web/src/lib/density.ts) and `applyPrefLocale` (web/src/i18n) each return
|
|
725
|
+
// early on a value the account does not carry. This was the odd one out.
|
|
726
|
+
//
|
|
727
|
+
// ⚠⚠ Silence is not the same as an invalid id, either: `setCustomThemes`
|
|
728
|
+
// deliberately calls this with `DEFAULT_THEME_ID` to demote somebody off a
|
|
729
|
+
// theme the operator deleted, and that IS an answer and must still paint.
|
|
730
|
+
if (!id) return;
|
|
731
|
+
const valid = plausibleThemeId(id) ? id : DEFAULT_THEME_ID;
|
|
732
|
+
themeId.value = valid;
|
|
733
|
+
setLocalPref('palette', valid);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Paint a palette this person did not choose — the instance's own default,
|
|
738
|
+
* applied to somebody who has never expressed a preference.
|
|
739
|
+
*
|
|
740
|
+
* ⚠⚠ It records NOTHING: not the account, not this browser's mirror. An
|
|
741
|
+
* instance default is the operator's standing answer, not a personal choice,
|
|
742
|
+
* and the difference is load-bearing in both directions. Written to the
|
|
743
|
+
* account, it would overwrite on one device a choice the person made on
|
|
744
|
+
* another. Written to the mirror, it would make `hasOwnChoice()` true, so the
|
|
745
|
+
* day the operator picks a different house theme every existing person would
|
|
746
|
+
* stay pinned to the old one, permanently, with no way to tell why.
|
|
747
|
+
*/
|
|
748
|
+
export function applyInstanceDefault(id: string | undefined | null): void {
|
|
749
|
+
if (id && plausibleThemeId(id)) themeId.value = id;
|
|
528
750
|
}
|
|
529
751
|
|
|
530
752
|
/** Reactive handle for components: `{ themeId, setTheme }`. */
|
|
@@ -552,9 +774,23 @@ export type ThemeModePref = 'host' | 'auto' | 'light' | 'dark';
|
|
|
552
774
|
|
|
553
775
|
export const THEME_MODE_LS_KEY = 'filex.thememode';
|
|
554
776
|
|
|
777
|
+
/**
|
|
778
|
+
* ⚠ It is a PERSON's key, so a sign-out takes it with the rest
|
|
779
|
+
* (`lib/prefs` → `forgetPersonalPrefs`). Registered from here rather than
|
|
780
|
+
* listed over there because a storage key written down in two modules is the
|
|
781
|
+
* drift `lib/density`' header was written about.
|
|
782
|
+
*/
|
|
783
|
+
registerPersonalMirror(THEME_MODE_LS_KEY);
|
|
784
|
+
|
|
555
785
|
const MODE_VALUES: ThemeModePref[] = ['host', 'auto', 'light', 'dark'];
|
|
556
786
|
|
|
557
787
|
function readStoredThemeMode(): ThemeModePref {
|
|
788
|
+
// ⚠⚠ Same rule as the palette, same reason: light or dark is a PERSON's
|
|
789
|
+
// answer. `'host'` is the neutral one — the explorer then falls back to
|
|
790
|
+
// `config.theme || 'auto'` (FileExplorer.vue) and `'auto'` is
|
|
791
|
+
// `prefers-color-scheme`, which is exactly what the owner asked a signed-out
|
|
792
|
+
// window to follow.
|
|
793
|
+
if (!hasSession()) return 'host';
|
|
558
794
|
try {
|
|
559
795
|
const v = localStorage.getItem(THEME_MODE_LS_KEY) as ThemeModePref | null;
|
|
560
796
|
return v && MODE_VALUES.includes(v) ? v : 'host';
|
|
@@ -578,6 +814,30 @@ export function setThemeMode(mode: ThemeModePref): void {
|
|
|
578
814
|
}
|
|
579
815
|
}
|
|
580
816
|
|
|
817
|
+
/**
|
|
818
|
+
* The session answer changed — re-decide whose look this window wears.
|
|
819
|
+
*
|
|
820
|
+
* ⚠⚠ Needed because the two reads above happen at MODULE LOAD, and the only
|
|
821
|
+
* synchronous evidence available then is `hasSession()`'s stored hint. Two
|
|
822
|
+
* things can contradict it later and both must be obeyed:
|
|
823
|
+
* • the document turns out to be a public link (`sealSessionless` in the
|
|
824
|
+
* host's boot), and
|
|
825
|
+
* • `/api/auth/me` answers "nobody" on a browser whose hint said otherwise —
|
|
826
|
+
* a session that expired, or somebody who closed the tab instead of
|
|
827
|
+
* signing out. That is the shared-machine case, and leaving it unhandled
|
|
828
|
+
* would keep the leak alive for exactly the people most likely to hit it.
|
|
829
|
+
*
|
|
830
|
+
* ⚠ It re-READS rather than assuming: called after a sign-in it restores the
|
|
831
|
+
* person's own palette from the mirror, so the function is one answer to the
|
|
832
|
+
* question, not a one-way demotion.
|
|
833
|
+
*
|
|
834
|
+
* ⚠ It touches no storage. Nothing here is a choice anybody made.
|
|
835
|
+
*/
|
|
836
|
+
export function resolveSessionLook(): void {
|
|
837
|
+
themeId.value = readStoredThemeId();
|
|
838
|
+
themeMode.value = readStoredThemeMode();
|
|
839
|
+
}
|
|
840
|
+
|
|
581
841
|
/** Reactive handle for components: `{ themeMode, setThemeMode }`. */
|
|
582
842
|
export function useThemeModeState(): {
|
|
583
843
|
themeMode: Ref<ThemeModePref>;
|
|
@@ -587,10 +847,16 @@ export function useThemeModeState(): {
|
|
|
587
847
|
}
|
|
588
848
|
|
|
589
849
|
// Cross-tab sync, same contract as the theme id above.
|
|
850
|
+
//
|
|
851
|
+
// ⚠⚠ Gated on the session for the same reason the boot read is. A `storage`
|
|
852
|
+
// event is broadcast to EVERY tab on the origin, so a signed-in tab changing
|
|
853
|
+
// the mode would otherwise push that person's answer into a tab sitting on the
|
|
854
|
+
// sign-in page or on somebody's share link — the leak coming back through a
|
|
855
|
+
// side door after the front one was shut.
|
|
590
856
|
if (typeof window !== 'undefined') {
|
|
591
857
|
try {
|
|
592
858
|
window.addEventListener('storage', (e) => {
|
|
593
|
-
if (e.key !== THEME_MODE_LS_KEY) return;
|
|
859
|
+
if (e.key !== THEME_MODE_LS_KEY || !hasSession()) return;
|
|
594
860
|
const v = e.newValue as ThemeModePref | null;
|
|
595
861
|
themeMode.value = v && MODE_VALUES.includes(v) ? v : 'host';
|
|
596
862
|
});
|
|
@@ -603,8 +869,10 @@ if (typeof window !== 'undefined') {
|
|
|
603
869
|
if (typeof window !== 'undefined') {
|
|
604
870
|
try {
|
|
605
871
|
window.addEventListener('storage', (e) => {
|
|
606
|
-
|
|
607
|
-
|
|
872
|
+
// ⚠ `hasSession()` — see the mode listener above. Same event, same
|
|
873
|
+
// origin-wide broadcast, same leak.
|
|
874
|
+
if (e.key !== THEME_LS_KEY || !hasSession()) return;
|
|
875
|
+
themeId.value = plausibleThemeId(e.newValue) ? (e.newValue as string) : DEFAULT_THEME_ID;
|
|
608
876
|
});
|
|
609
877
|
} catch {
|
|
610
878
|
/* non-browser env */
|
|
@@ -622,7 +890,7 @@ if (typeof window !== 'undefined') {
|
|
|
622
890
|
* root subtree is always correct regardless of host CSS.
|
|
623
891
|
*/
|
|
624
892
|
export function applyThemeToEl(el: HTMLElement, id: string, dark: boolean): void {
|
|
625
|
-
for (const key of
|
|
893
|
+
for (const key of allTokenKeys()) el.style.removeProperty(key);
|
|
626
894
|
if (id === DEFAULT_THEME_ID) return;
|
|
627
895
|
const theme = themeById(id);
|
|
628
896
|
if (!theme) return;
|