@morphemecms/cms 0.11.0 → 0.13.0-next-20261106203613
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/CHANGELOG.md +31 -0
- package/astro.mjs +2 -1
- package/defaults/blockTexts.ts +9 -9
- package/defaults/collections.ts +257 -257
- package/defaults/colors.ts +74 -74
- package/defaults/company.ts +9 -9
- package/defaults/editor.ts +65 -65
- package/defaults/i18n/da-DK/blockTexts.ts +12 -0
- package/defaults/i18n/da-DK/collections.ts +220 -0
- package/defaults/i18n/da-DK/colors.ts +102 -0
- package/defaults/i18n/da-DK/company.ts +7 -0
- package/defaults/i18n/da-DK/editor.ts +13 -0
- package/defaults/i18n/da-DK/index.ts +31 -0
- package/defaults/i18n/da-DK/internalLinks.ts +12 -0
- package/defaults/i18n/da-DK/opens.ts +7 -0
- package/defaults/i18n/da-DK/popup.ts +9 -0
- package/defaults/i18n/da-DK/privacy.ts +38 -0
- package/defaults/i18n/da-DK/rules.ts +56 -0
- package/defaults/i18n/da-DK/sitedata.ts +8 -0
- package/defaults/i18n/da-DK/standard.ts +8 -0
- package/defaults/i18n/da-DK/standardPages.ts +17 -0
- package/defaults/internalLinks.ts +8 -8
- package/defaults/opens.ts +2 -2
- package/defaults/popup.tsx +7 -7
- package/defaults/privacy.ts +37 -37
- package/defaults/rules.ts +68 -66
- package/defaults/schema.ts +1 -1
- package/defaults/sitedata.ts +4 -4
- package/defaults/standard.ts +9 -6
- package/defaults/standardPages.ts +14 -14
- package/defaults/texts.ts +7 -510
- package/lib/appId.d.mts +1 -0
- package/lib/appId.mjs +6 -0
- package/lib/appRoute.d.mts +2 -2
- package/lib/appRoute.mjs +3 -3
- package/lib/appRoute.test.mjs +8 -8
- package/lib/apps.mjs +13 -3
- package/lib/apps.test.mjs +10 -1
- package/lib/contracts.d.mts +3 -2
- package/lib/contracts.mjs +3 -2
- package/lib/folderChanges.d.mts +29 -0
- package/lib/folderChanges.mjs +34 -0
- package/lib/licenses.mjs +2 -2
- package/lib/licenses.owner.test.mjs +9 -0
- package/lib/menu.mjs +4 -4
- package/lib/menu.test.mjs +101 -101
- package/lib/scheduled.mjs +1 -1
- package/lib/section-ranges.d.mts +1 -1
- package/lib/section-ranges.mjs +8 -14
- package/lib/section-ranges.test.mjs +26 -5
- package/lib/storeEntries.d.mts +3 -1
- package/lib/storeEntries.mjs +22 -8
- package/lib/storeEntries.test.mjs +59 -7
- package/lib/text-sections.d.mts +11 -1
- package/lib/text-sections.mjs +18 -8
- package/lib/text-sections.test.mjs +34 -1
- package/lib/translate.d.mts +11 -0
- package/lib/translate.mjs +13 -1
- package/lib/translate.test.mjs +23 -2
- package/lib/video-id.d.mts +6 -2
- package/lib/video-id.mjs +5 -1
- package/lib/video.d.mts +6 -2
- package/lib/video.mjs +5 -1
- package/lib/without-h1.d.mts +1 -0
- package/lib/without-h1.mjs +19 -4
- package/lib/without-h1.test.mjs +30 -1
- package/package.json +3 -2
- package/scripts/app-screenshots.mjs +9 -8
- package/scripts/check-content.mjs +12 -2
- package/scripts/check-language.mjs +0 -1
- package/scripts/check-site-types.mjs +26 -0
- package/scripts/editor-screens.mjs +3 -3
- package/scripts/site-types.mjs +14 -0
- package/scripts/site-types.test.mjs +21 -0
- package/site/activeTheme.test.mjs +52 -0
- package/site/appSelf.d.mts +2 -0
- package/site/appSelf.mjs +23 -0
- package/site/appSelf.test.mjs +35 -0
- package/site/content.ts +5 -2
- package/site/defineSite.mjs +2 -2
- package/site/schema.ts +3 -3
- package/site/siteModules.mjs +2 -1
- package/site/theme.mjs +19 -8
- package/src/Advanced.tsx +2 -2
- package/src/App.tsx +79 -78
- package/src/AppAdds.tsx +1 -1
- package/src/AppList.tsx +39 -24
- package/src/BlockDialog.tsx +39 -18
- package/src/BlockParts.tsx +6 -6
- package/src/BlockSettings.tsx +2 -2
- package/src/BrandForm.tsx +2 -2
- package/src/Colors.tsx +6 -6
- package/src/CompanyForm.tsx +1 -1
- package/src/Consents.tsx +3 -3
- package/src/CreateFab.tsx +8 -0
- package/src/DataForm.tsx +10 -20
- package/src/Deleted.tsx +3 -3
- package/src/EditorFooter.tsx +2 -2
- package/src/EntryForm.tsx +3 -3
- package/src/FooterSettings.tsx +2 -2
- package/src/FormFrame.tsx +4 -3
- package/src/HeaderSettings.tsx +3 -3
- package/src/Keywords.tsx +5 -5
- package/src/Library.tsx +75 -82
- package/src/Licenses.tsx +8 -8
- package/src/ListFab.tsx +13 -0
- package/src/MainDrawerParts.tsx +3 -3
- package/src/MainMenu.tsx +1 -1
- package/src/MarkdownEditor.tsx +88 -47
- package/src/Media.tsx +11 -20
- package/src/MediaTags.tsx +2 -2
- package/src/Notifications.tsx +2 -2
- package/src/Parts.tsx +17 -17
- package/src/Redirects.tsx +14 -22
- package/src/Screen.tsx +16 -6
- package/src/SeoScores.tsx +6 -6
- package/src/SideColumn.tsx +2 -2
- package/src/StartFab.tsx +6 -8
- package/src/ThemeStore.tsx +37 -16
- package/src/Tokens.tsx +2 -2
- package/src/apps.ts +7 -8
- package/src/checkSite.ts +86 -20
- package/src/drawerYield.d.mts +3 -0
- package/src/drawerYield.mjs +10 -0
- package/src/drawerYield.test.mjs +22 -0
- package/src/editor.css +59 -38
- package/src/examples.ts +1 -1
- package/src/fabScreens.test.mjs +28 -0
- package/src/fabScreens.ts +16 -0
- package/src/i18n/da-DK/library.ts +2 -2
- package/src/i18n/da-DK/text.ts +1 -1
- package/src/i18n/en-US/apps.ts +1 -1
- package/src/i18n/en-US/common.ts +8 -8
- package/src/i18n/en-US/library.ts +5 -5
- package/src/i18n/en-US/parts.ts +3 -3
- package/src/i18n/en-US/text.ts +1 -1
- package/src/i18n/index.ts +4 -8
- package/src/images.ts +2 -2
- package/src/legacyMove.test.mjs +137 -0
- package/src/legacyMove.ts +131 -0
- package/src/licenseData.ts +3 -3
- package/src/markdown.mjs +10 -7
- package/src/markdown.test.mjs +16 -2
- package/src/menuChildren.ts +5 -5
- package/src/menuOpen.ts +20 -4
- package/src/movedRoutes.d.mts +8 -1
- package/src/movedRoutes.mjs +102 -21
- package/src/movedRoutes.test.mjs +128 -6
- package/src/nav.ts +1 -1
- package/src/notificationData.ts +1 -1
- package/src/pageH1Panel.d.mts +4 -0
- package/src/pageH1Panel.mjs +31 -0
- package/src/pageH1Panel.test.mjs +56 -0
- package/src/pagePreview.tsx +22 -12
- package/src/schema.ts +2 -2
- package/src/sectionFrame.ts +3 -3
- package/src/sectionState.d.mts +3 -3
- package/src/sectionState.mjs +6 -6
- package/src/sectionState.test.mjs +14 -2
- package/src/sheetPlacement.d.mts +6 -4
- package/src/sheetPlacement.mjs +15 -10
- package/src/sheetPlacement.test.mjs +16 -9
- package/src/sidePanel.ts +38 -0
- package/src/site.ts +37 -13
- package/src/standard/accordion/index.tsx +1 -1
- package/src/standard/accordion/shared.ts +1 -1
- package/src/standard/buttons/index.tsx +1 -1
- package/src/standard/cards/index.tsx +1 -1
- package/src/standard/cards/shared.ts +1 -1
- package/src/standard/cookies/index.tsx +1 -1
- package/src/standard/posts/index.tsx +1 -1
- package/src/standard/testimonials/index.tsx +1 -1
- package/src/standard/testimonials/list.ts +1 -1
- package/src/store.ts +36 -31
- package/src/tagUses.d.mts +1 -1
- package/src/tagUses.mjs +2 -2
- package/src/tagUses.test.mjs +1 -1
- package/src/topBarSlot.tsx +7 -15
- package/src/ui.tsx +33 -22
- package/src/virtual.d.ts +6 -0
- package/starter/package.json +1 -1
- package/theme/components/PageHeading.astro +22 -0
- package/theme/layouts/Base.astro +2 -2
- package/theme/layouts/PageLayout.astro +37 -20
- package/theme/styles/global.css +2 -1
- package/theme/styles/ui.css +40 -10
package/src/BlockDialog.tsx
CHANGED
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// (the standard blocks, each app's under its name), the one picked as a chip. Also the sheet of a section's
|
|
8
8
|
// settings (its colour, SectionSettings.tsx), which goes the same way as a block's fields: Annuller, Gem, the question when closed changed.
|
|
9
9
|
// The fields and the section settings are in the theme's side sheet (sheetPlacement.mjs): a standard one beside the page from
|
|
10
|
-
//
|
|
10
|
+
// expanded (not modal: the page stays usable, .editor-side-sheet in editor.css), a modal one over the page below it
|
|
11
11
|
// (fields only; the section settings are a sheet from the bottom there). Both end with Gem (Indsæt) and Annuller.
|
|
12
12
|
// Their open, close, focus and Escape are the theme's (sideSheet() and standardSideSheet(), sideSheetControl.ts); leave() is their beforeClose.
|
|
13
|
-
// Another block tapped while one is open closes the open one first (the question when changed).
|
|
14
|
-
|
|
13
|
+
// Another block tapped while one is open closes the open one first (the question when changed). In the standard side sheet a
|
|
14
|
+
// card already in the text also has "Remove" after Cancel in the actions (the answer 'remove'). Exposes useBlockDialog, IMAGE.
|
|
15
|
+
import { useEffect, useId, useRef, useState, type CSSProperties, type ReactElement } from 'react';
|
|
15
16
|
import { ArrowsDownUp, Image } from '@phosphor-icons/react';
|
|
16
17
|
import {
|
|
17
18
|
Button,
|
|
@@ -46,6 +47,7 @@ import { snackbar } from '../lib/snackbar';
|
|
|
46
47
|
import { closeChanged } from './ask';
|
|
47
48
|
import SectionSettings from './SectionSettings';
|
|
48
49
|
import { sheetLook } from './sheetPlacement.mjs';
|
|
50
|
+
import { setPanelOpen } from './sidePanel';
|
|
49
51
|
import { SiteSideSheet, siteSideSheet, siteSideSheetDialogClass, siteStandardSideSheet } from './themeUi';
|
|
50
52
|
|
|
51
53
|
export type BlockAsk = {
|
|
@@ -58,13 +60,17 @@ export type BlockAsk = {
|
|
|
58
60
|
* than once, unless the template says how many (have: the types in the text now, one for each, IMAGE for a picture).
|
|
59
61
|
* image: a picture is offered first, and chosen as the type IMAGE. */
|
|
60
62
|
choose: (offer?: { image?: boolean; have?: string[] }) => Promise<{ type: string; props: BlockProps } | null>;
|
|
61
|
-
/** New props for a block in the text; null when cancelled. isNew: one about to be put in ("Indsæt").
|
|
62
|
-
|
|
63
|
+
/** New props for a block in the text; null when cancelled. isNew: one about to be put in ("Indsæt"). removable: the
|
|
64
|
+
* card may be removed, so the side panel has "Remove"; 'remove' when it is tapped (never when removable is not true). */
|
|
65
|
+
edit: (type: string, props: BlockProps, isNew?: boolean, removable?: boolean) => Promise<BlockProps | 'remove' | null>;
|
|
63
66
|
/** The settings of the section the cursor is in (now its colour; null: none); the new ones, or null when cancelled. */
|
|
64
67
|
section: (color: string | null) => Promise<{ color: string | null } | null>;
|
|
65
68
|
};
|
|
66
69
|
|
|
67
|
-
type Open =
|
|
70
|
+
type Open =
|
|
71
|
+
| { step: 'choose'; image?: boolean; have?: string[] }
|
|
72
|
+
| { step: 'fields'; type: string; isNew: boolean; removable?: boolean }
|
|
73
|
+
| { step: 'section' };
|
|
68
74
|
|
|
69
75
|
/** The type a section's settings are answered with (not a block's). */
|
|
70
76
|
const SECTION = 'section';
|
|
@@ -99,7 +105,7 @@ const picture: SiteBlock = {
|
|
|
99
105
|
},
|
|
100
106
|
],
|
|
101
107
|
};
|
|
102
|
-
type Answer = { type: string; props: BlockProps } | null;
|
|
108
|
+
type Answer = { type: string; props: BlockProps } | 'remove' | null;
|
|
103
109
|
|
|
104
110
|
// Text as searched: lower case, without accents (as in PagePicker.tsx).
|
|
105
111
|
const plain = (s: string) =>
|
|
@@ -110,7 +116,7 @@ const plain = (s: string) =>
|
|
|
110
116
|
|
|
111
117
|
type InsertSort = 'az' | 'za' | 'group';
|
|
112
118
|
const INSERT_SORTS: InsertSort[] = ['az', 'za', 'group'];
|
|
113
|
-
const INSERT_SORT_KEY = 'editor-sort-
|
|
119
|
+
const INSERT_SORT_KEY = 'editor-sort-insert';
|
|
114
120
|
|
|
115
121
|
// A block's group: the app's name for an app's block, otherwise the standard blocks (also the picture and HTML).
|
|
116
122
|
const groupOf = (b: SiteBlock) => (b.app ? (apps.find((a) => a.id === b.app)?.label ?? b.app) : t.blockSettings.standard);
|
|
@@ -124,13 +130,13 @@ export function useBlockDialog(
|
|
|
124
130
|
): [BlockAsk, ReactElement] {
|
|
125
131
|
const blockOf = (type: string) => (type === IMAGE ? picture : type === HTML ? htmlBlock : list.find((b) => b.type === type));
|
|
126
132
|
const dialog = useRef<HTMLDialogElement>(null);
|
|
127
|
-
// The modal side sheet's <dialog> (below
|
|
133
|
+
// The modal side sheet's <dialog> (below expanded) and its controls; the standard one's box (from expanded).
|
|
128
134
|
const sideDialog = useRef<HTMLDialogElement>(null);
|
|
129
135
|
const sideControls = useRef<ReturnType<typeof siteSideSheet> | null>(null);
|
|
130
136
|
const standard = useRef<HTMLDivElement>(null);
|
|
131
137
|
const standardControls = useRef<ReturnType<typeof siteStandardSideSheet> | null>(null);
|
|
132
138
|
const headingId = useId();
|
|
133
|
-
const
|
|
139
|
+
const expanded = useAtLeast('expanded');
|
|
134
140
|
const answer = useRef<(a: Answer) => void>(() => {});
|
|
135
141
|
const [open, setOpen] = useState<Open | null>(null);
|
|
136
142
|
// What is open now (set at once, where `open` follows a render later), the look it has, when the last one has closed
|
|
@@ -186,7 +192,7 @@ export function useBlockDialog(
|
|
|
186
192
|
};
|
|
187
193
|
|
|
188
194
|
// Which dialog is open follows the look of the step: the sheet from the bottom, or the modal side sheet (below).
|
|
189
|
-
const look: string = open ? sheetLook(open.step,
|
|
195
|
+
const look: string = open ? sheetLook(open.step, expanded) : 'sheet';
|
|
190
196
|
lookNow.current = look;
|
|
191
197
|
useEffect(() => {
|
|
192
198
|
const sheet = dialog.current;
|
|
@@ -202,7 +208,7 @@ export function useBlockDialog(
|
|
|
202
208
|
return false;
|
|
203
209
|
};
|
|
204
210
|
|
|
205
|
-
// The modal side sheet (below
|
|
211
|
+
// The modal side sheet (below expanded).
|
|
206
212
|
useEffect(() => {
|
|
207
213
|
const d = sideDialog.current;
|
|
208
214
|
if (look !== 'modal-side-sheet' || !d) return;
|
|
@@ -214,11 +220,12 @@ export function useBlockDialog(
|
|
|
214
220
|
};
|
|
215
221
|
}, [look]);
|
|
216
222
|
|
|
217
|
-
// The standard side sheet (from
|
|
223
|
+
// The standard side sheet (from expanded): the page makes room for it (editor.css: [data-side-panel]), the focus moves
|
|
218
224
|
// into it and back to what had it before when it closes.
|
|
219
225
|
useEffect(() => {
|
|
220
226
|
if (look !== 'side-sheet') return;
|
|
221
227
|
document.documentElement.setAttribute('data-side-panel', '');
|
|
228
|
+
setPanelOpen(true);
|
|
222
229
|
const box = standard.current?.querySelector<HTMLElement>('[data-side-sheet-container]');
|
|
223
230
|
if (box) {
|
|
224
231
|
standardControls.current = siteStandardSideSheet(box, { beforeClose });
|
|
@@ -227,6 +234,7 @@ export function useBlockDialog(
|
|
|
227
234
|
return () => {
|
|
228
235
|
standardControls.current = null;
|
|
229
236
|
document.documentElement.removeAttribute('data-side-panel');
|
|
237
|
+
setPanelOpen(false);
|
|
230
238
|
};
|
|
231
239
|
}, [look]);
|
|
232
240
|
|
|
@@ -254,16 +262,23 @@ export function useBlockDialog(
|
|
|
254
262
|
list,
|
|
255
263
|
template,
|
|
256
264
|
data,
|
|
257
|
-
choose: (offer) =>
|
|
265
|
+
choose: async (offer) => {
|
|
266
|
+
const answered = await ask({ step: 'choose', image: offer?.image, have: offer?.have });
|
|
267
|
+
return answered === 'remove' ? null : answered;
|
|
268
|
+
},
|
|
258
269
|
section: async (color) => {
|
|
259
270
|
const answered = await ask({ step: 'section' }, { color: color ?? '' });
|
|
260
|
-
|
|
271
|
+
if (!answered || answered === 'remove') return null;
|
|
272
|
+
return { color: answered.props.color ? String(answered.props.color) : null };
|
|
261
273
|
},
|
|
262
|
-
edit: async (type, props, isNew = false) => {
|
|
274
|
+
edit: async (type, props, isNew = false, removable = false) => {
|
|
263
275
|
const fields = blockOf(type)?.fields ?? [];
|
|
264
276
|
// A block on the page with what a field it has no value for means; a new one with the defaults.
|
|
265
|
-
const a = await ask(
|
|
266
|
-
|
|
277
|
+
const a = await ask(
|
|
278
|
+
{ step: 'fields', type, isNew, removable },
|
|
279
|
+
isNew ? { ...newValue(fields), ...props } : savedValue(fields, props),
|
|
280
|
+
);
|
|
281
|
+
return a === 'remove' ? a : a && a.props;
|
|
267
282
|
},
|
|
268
283
|
};
|
|
269
284
|
|
|
@@ -378,6 +393,12 @@ export function useBlockDialog(
|
|
|
378
393
|
<Button kind="outlined" onClick={() => void leave()}>
|
|
379
394
|
{t.common.cancel}
|
|
380
395
|
</Button>
|
|
396
|
+
{open.step === 'fields' && open.removable && !open.isNew && look === 'side-sheet' && (
|
|
397
|
+
// After Save and Cancel in the start-aligned row, in the error colour (the theme's text button with its colour variable).
|
|
398
|
+
<Button kind="text" style={{ '--button-text-text': 'var(--error)' } as CSSProperties} onClick={() => done('remove')}>
|
|
399
|
+
{t.common.remove}
|
|
400
|
+
</Button>
|
|
401
|
+
)}
|
|
381
402
|
</>
|
|
382
403
|
}
|
|
383
404
|
>
|
package/src/BlockParts.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// "Dele" › "UI-komponenter" (#
|
|
2
|
-
// ("Promovering", #
|
|
1
|
+
// "Dele" › "UI-komponenter" (#appearance/parts/ui), "Dele" › "Blokke" (#appearance/parts/blocks) and "Dele" › a list of kinds
|
|
2
|
+
// ("Promovering", #appearance/parts/list/<id>): how the site looks, every part in every version, to see and agree on it.
|
|
3
3
|
// "UI-komponenter" also has the active theme's coverage (summary, mark on each part, filter by style) and the site frame's parts.
|
|
4
4
|
// "UI-komponenter" has the theme's UI components (its buttons and icon buttons, text fields and checkboxes …,
|
|
5
5
|
// packages/editor/theme/ui/, through @site/editor) in every kind, size and state; "Blokke" every block, the standard
|
|
@@ -52,7 +52,7 @@ import { t } from './i18n';
|
|
|
52
52
|
import { Svg } from './icon';
|
|
53
53
|
|
|
54
54
|
const { screens } = t.parts;
|
|
55
|
-
const back = { href: '#
|
|
55
|
+
const back = { href: '#appearance/parts', label: t.common.parts.editorParts };
|
|
56
56
|
|
|
57
57
|
// The fields filled in with the plain picture where they have none (a card's, a list's items').
|
|
58
58
|
function pictured(fields: Field[], props: BlockProps): BlockProps {
|
|
@@ -520,7 +520,7 @@ function CardParts() {
|
|
|
520
520
|
);
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
/** "Dele" › "UI-komponenter" (#
|
|
523
|
+
/** "Dele" › "UI-komponenter" (#appearance/parts/ui): the theme's UI components in every kind, size and state. */
|
|
524
524
|
export function ComponentParts() {
|
|
525
525
|
const groups = [
|
|
526
526
|
{ id: 'ui', title: screens.ui },
|
|
@@ -587,7 +587,7 @@ export function ComponentParts() {
|
|
|
587
587
|
);
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
-
/** "Dele" › "Blokke" (#
|
|
590
|
+
/** "Dele" › "Blokke" (#appearance/parts/blocks): every block, the standard ones and then the apps', each a group of its
|
|
591
591
|
* own, in all its versions. */
|
|
592
592
|
export function BlocksParts() {
|
|
593
593
|
const data = useMemo(exampleData, []);
|
|
@@ -616,7 +616,7 @@ export function BlocksParts() {
|
|
|
616
616
|
/** The lists of kinds ("Promovering"), each a screen of "Dele". */
|
|
617
617
|
export const kindLists = () => collections.filter((c): c is FolderCollection => c.kind === 'folder' && !!c.kinds && !!c.fields?.length);
|
|
618
618
|
|
|
619
|
-
/** "Dele" › a list of kinds ("Promovering", #
|
|
619
|
+
/** "Dele" › a list of kinds ("Promovering", #appearance/parts/list/<id>): each of its kinds as the site draws it, its own
|
|
620
620
|
* (FolderCollection.draws: the pop-up, the bars) and the apps' (AppKind: Mailchimp's signup, once for each of its
|
|
621
621
|
* states), each a group of its own, as a new one of the kind with the made-up texts (kindExample). */
|
|
622
622
|
export function KindParts({ id }: { id: string }) {
|
package/src/BlockSettings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Blokke" › "Blokindstillinger" (#
|
|
1
|
+
// "Blokke" › "Blokindstillinger" (#block-settings): the blocks that can be put in the text of a page, in
|
|
2
2
|
// groups, the editor's standard blocks (packages/editor/src/standard/) and those of each app installed under "Apps",
|
|
3
3
|
// under the app's name (e.g. "Mailchimp",
|
|
4
4
|
// src/apps/, AppList.tsx; an app not installed has none here), each with how many pages it is on at its right
|
|
@@ -243,7 +243,7 @@ export function KindSwitch({ app, list, env, files }: { app: SiteApp; list: stri
|
|
|
243
243
|
type Sort = 'group' | 'az' | 'za';
|
|
244
244
|
const SORTS: Sort[] = ['group', 'az', 'za'];
|
|
245
245
|
// The order picked last time, in this browser.
|
|
246
|
-
const SORT_KEY = 'editor-sort-
|
|
246
|
+
const SORT_KEY = 'editor-sort-block-settings';
|
|
247
247
|
type OnOff = '' | 'on' | 'off';
|
|
248
248
|
type Use = '' | 'used' | 'unused';
|
|
249
249
|
|
package/src/BrandForm.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Logo og ikon" (#
|
|
1
|
+
// "Udseende" › "Logo og ikon" (#appearance/logo): a new logo (the mark by the site's name) and a new icon (the browser
|
|
2
2
|
// tab, Google and the editor's app icon), made from a picture the owner picks (brand.ts), in the site's colours
|
|
3
3
|
// ("Farver"). Both are saved together in one commit with "Gem ændringer".
|
|
4
4
|
import { useEffect, useState } from 'react';
|
|
@@ -120,7 +120,7 @@ export default function BrandForm({ env }: { env: Env }) {
|
|
|
120
120
|
return (
|
|
121
121
|
<FormFrame
|
|
122
122
|
title={t.common.parts.brand}
|
|
123
|
-
back={{ href: '#
|
|
123
|
+
back={{ href: '#appearance', label: t.common.parts.appearance }}
|
|
124
124
|
dirty={dirty}
|
|
125
125
|
saving={saving || drawing}
|
|
126
126
|
onSave={save}
|
package/src/Colors.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Farver" (#
|
|
1
|
+
// "Udseende" › "Farver" (#appearance/colors): the site's colours, for light and dark mode, in the site's file of
|
|
2
2
|
// colours (config.colors, packages/editor/lib/colors.ts). The first tab, "Generator", where the screen starts, or one
|
|
3
3
|
// mode at a time, picked with the tabs after it, and the editor is shown in the mode while the screen is open
|
|
4
4
|
// (theme.ts holdTheme). Each mode has a little page drawn in its colours, the pairs that are too hard to
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// farver", src/apps/content-colors/: pageColors in packages/editor/site/pageColors.ts). While an app installed changes the pages'
|
|
16
16
|
// colours (hooks/page-colors.ts), each mode shows the pages' colours instead (PageColors), as the site was last built
|
|
17
17
|
// (/editor/page-colors.json, packages/editor/scripts/page-colors.mjs): first a notice that the apps give them, with a button to the
|
|
18
|
-
// site's own, which are only the fallback ("Reservefarver", #
|
|
18
|
+
// site's own, which are only the fallback ("Reservefarver", #appearance/colors/fallback-colors, a page of the same form, so
|
|
19
19
|
// what is unsaved stays), then the apps, each to its settings, the pages with colours from a picture (the picture, the
|
|
20
20
|
// page and its colours in the mode open), and those with the site's own. Saved with "Gem ændringer" in one commit; the
|
|
21
21
|
// editor shows the new colours at once (theme.ts applyColors), the site when it is built again.
|
|
@@ -112,7 +112,7 @@ const CHANGE = t.app.changes.edited(t.app.changes.colors);
|
|
|
112
112
|
type Tab = Mode | 'generator';
|
|
113
113
|
|
|
114
114
|
/** The page with the site's own colours, while an app changes the pages' colours: the fallback. */
|
|
115
|
-
export const FALLBACK_ROUTE = '
|
|
115
|
+
export const FALLBACK_ROUTE = 'appearance/colors/fallback-colors';
|
|
116
116
|
|
|
117
117
|
/** The apps installed that change the pages' colours (hooks/page-colors.ts). */
|
|
118
118
|
const colorApps = (files: RepoFile[]) => {
|
|
@@ -139,7 +139,7 @@ export default function Colors({ env, files, fallback }: { env: Env; files: Repo
|
|
|
139
139
|
const changing = useMemo(() => colorApps(files), [files]);
|
|
140
140
|
useEffect(() => () => holdTheme(null), []);
|
|
141
141
|
// While an app changes the pages' colours, the modes show the pages', and the site's own are on a page of the form.
|
|
142
|
-
useFormPages(changing.length ? ['
|
|
142
|
+
useFormPages(changing.length ? ['appearance/colors', FALLBACK_ROUTE] : []);
|
|
143
143
|
const ownShown = fallback || !changing.length;
|
|
144
144
|
const appNames = new Intl.ListFormat(locale).format(changing.map((a) => a.label));
|
|
145
145
|
const file = files.find((f) => f.path === config.colors);
|
|
@@ -207,8 +207,8 @@ export default function Colors({ env, files, fallback }: { env: Env; files: Repo
|
|
|
207
207
|
title={changing.length && fallback ? t.colors.pages.fallback : t.common.parts.colors}
|
|
208
208
|
back={
|
|
209
209
|
changing.length && fallback
|
|
210
|
-
? { href: '#
|
|
211
|
-
: { href: '#
|
|
210
|
+
? { href: '#appearance/colors', label: t.common.parts.colors, guard: (go) => go() }
|
|
211
|
+
: { href: '#appearance', label: t.common.parts.appearance }
|
|
212
212
|
}
|
|
213
213
|
dirty={dirty}
|
|
214
214
|
saving={saving}
|
package/src/CompanyForm.tsx
CHANGED
|
@@ -113,7 +113,7 @@ export default function CompanyForm({ part, env, files, at: route, page }: Props
|
|
|
113
113
|
) : (
|
|
114
114
|
<FormFrame
|
|
115
115
|
title={part.label}
|
|
116
|
-
back={part.back ?? { href: '#
|
|
116
|
+
back={part.back ?? { href: '#company', label: t.common.parts.company }}
|
|
117
117
|
dirty={dirty}
|
|
118
118
|
saving={saving}
|
|
119
119
|
onSave={save}
|
package/src/Consents.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "
|
|
1
|
+
// "Consents" (#privacy/consents, Part.opens 'consents'): the visitors' answers in the cookie banner, as the site's
|
|
2
2
|
// Worker keeps them (consentData.ts, packages/editor/worker/consent.js), newest first, so the site can show that a visitor said yes. Over
|
|
3
3
|
// the list the search by consent id (the one the banner shows the visitor under "Cookieindstillinger", asked of the
|
|
4
4
|
// Worker, so older answers are found too) and the order (kept in this browser); in the top bar the download of all of
|
|
@@ -32,7 +32,7 @@ import { t } from './i18n';
|
|
|
32
32
|
|
|
33
33
|
type Sort = 'newest' | 'oldest';
|
|
34
34
|
const SORTS: Sort[] = ['newest', 'oldest'];
|
|
35
|
-
const SORT_KEY = 'editor-sort-
|
|
35
|
+
const SORT_KEY = 'editor-sort-consents';
|
|
36
36
|
|
|
37
37
|
// The yes and the no of an answer as one line, in the banner's order: "Ja: Statistik · Nej: Marketing".
|
|
38
38
|
function summary(answer: ConsentAnswer) {
|
|
@@ -46,7 +46,7 @@ function summary(answer: ConsentAnswer) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
export default function Consents({ env, back }: { env: Env; back: { href: string; label: string } }) {
|
|
49
|
-
const [query, setQuery] = useKept('
|
|
49
|
+
const [query, setQuery] = useKept('consents-search', '');
|
|
50
50
|
const [sort, setSort] = useState<Sort>(() => {
|
|
51
51
|
try {
|
|
52
52
|
const saved = localStorage.getItem(SORT_KEY);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// A screen's one create action as a FAB (Files, Redirects, a single-file list form): a button, busy and
|
|
2
|
+
// disabled as the screen says. Exposes CreateFab. Uses FabMenu (ui.tsx).
|
|
3
|
+
import { Plus } from '@phosphor-icons/react';
|
|
4
|
+
import { FabMenu } from './ui';
|
|
5
|
+
|
|
6
|
+
export function CreateFab({ label, onClick, busy, disabled }: { label: string; onClick: () => void; busy?: boolean; disabled?: boolean }) {
|
|
7
|
+
return <FabMenu icon={Plus} label={label} items={[{ icon: Plus, label, onClick, busy, disabled }]} />;
|
|
8
|
+
}
|
package/src/DataForm.tsx
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
// is opened as changes that are not saved. Exposes DataForm (default). Its values from the file and back: dataFileValues.ts;
|
|
5
5
|
// a menu's fields: DataFileFields.tsx.
|
|
6
6
|
import { useMemo, useState } from 'react';
|
|
7
|
-
import { Plus } from '@phosphor-icons/react';
|
|
8
7
|
import { entryName, slugify, slugOf, type DataFile, type FilesCollection, type FolderCollection } from './schema';
|
|
9
8
|
import { config, header, keptBy, sharing, siteName, usesOf } from '@site/editor';
|
|
10
9
|
import { apps, asWritten, collections } from './site';
|
|
@@ -12,6 +11,7 @@ import { kindDraw } from './apps';
|
|
|
12
11
|
import { ConflictError, type RepoFile } from './github';
|
|
13
12
|
import { clean, newItem, newValue, problemAt } from './check';
|
|
14
13
|
import { FieldInput, FieldList, Fields, useWrongField, WrongField, type FieldContext } from './fields';
|
|
14
|
+
import { CreateFab } from './CreateFab';
|
|
15
15
|
import { DataFileFields } from './DataFileFields';
|
|
16
16
|
import { dataOf, fieldsOf, listOnly, valuesOf } from './dataFileValues';
|
|
17
17
|
import SectionsField from './SectionsField';
|
|
@@ -21,7 +21,7 @@ import { splitPicture } from '../lib/crop.mjs';
|
|
|
21
21
|
import { useMediaPicker } from './Media';
|
|
22
22
|
import FormFrame, { savedStatus, useSavedAt } from './FormFrame';
|
|
23
23
|
import { savedOn, VersionNotice, VersionsButton, WithVersions, type ShownVersion } from './Versions';
|
|
24
|
-
import { Button, CheckList, FoldSection, FormColumns, Group,
|
|
24
|
+
import { Button, CheckList, FoldSection, FormColumns, Group, Preview, FormHeading, SharePreview, Notice, ListItem } from './ui';
|
|
25
25
|
import { SearchPreview } from './SearchPreview';
|
|
26
26
|
import { searchedAs } from './googleResult.mjs';
|
|
27
27
|
import { plain, SEO_FIELDS, seoRating, SHARE_FIELDS, sharedAs } from './seo';
|
|
@@ -34,7 +34,7 @@ type Props = {
|
|
|
34
34
|
dataFile?: DataFile;
|
|
35
35
|
file: RepoFile;
|
|
36
36
|
env: Env;
|
|
37
|
-
/** A new entry of this kind (FolderCollection.kinds), picked
|
|
37
|
+
/** A new entry of this kind (FolderCollection.kinds), picked in the list's create FAB menu. */
|
|
38
38
|
kind?: string;
|
|
39
39
|
};
|
|
40
40
|
|
|
@@ -294,7 +294,7 @@ function Form({
|
|
|
294
294
|
label: v.label,
|
|
295
295
|
node: draw({ ...values, ...v.props }, { lists: env.lists, testimonials: env.testimonials, relations: env.relations, site: env.site }),
|
|
296
296
|
}));
|
|
297
|
-
// A file that is a list (the menus) is added to with
|
|
297
|
+
// A file that is a list (the menus) is added to with the FAB, like the other lists; its own fields
|
|
298
298
|
// ("Aktiveret") stand above it.
|
|
299
299
|
const root = dataFile?.root;
|
|
300
300
|
// Where the entry is used on the site (a tag: its posts, testimonials, promotions and blocks), each opening it.
|
|
@@ -309,28 +309,16 @@ function Form({
|
|
|
309
309
|
title={dataFile ? dataFile.label : isNew ? collection.create! : entryName(folder!, clean(fields, initial)) || kindLabel}
|
|
310
310
|
back={
|
|
311
311
|
header.includes(collection.id)
|
|
312
|
-
? { href: '#
|
|
312
|
+
? { href: '#appearance/header', label: t.common.parts.header }
|
|
313
313
|
: collection.id === 'footer'
|
|
314
|
-
? { href: '#
|
|
314
|
+
? { href: '#appearance/footer', label: t.common.parts.footer }
|
|
315
315
|
: collection.kind === 'files' && collection.files.length === 1
|
|
316
316
|
? { href: '#', label: siteName }
|
|
317
|
-
: { href: `#
|
|
317
|
+
: { href: `#list/${collection.id}`, label: collection.label }
|
|
318
318
|
}
|
|
319
319
|
status={fixed ? savedStatus({ savedAt, saving, deleting }) : undefined}
|
|
320
320
|
view={view}
|
|
321
|
-
actions={
|
|
322
|
-
<>
|
|
323
|
-
{!isNew && <VersionsButton env={env} path={base.path} onPick={onVersion} />}
|
|
324
|
-
{root && (
|
|
325
|
-
<IconButton
|
|
326
|
-
icon={Plus}
|
|
327
|
-
label={t.forms.addOne(root.singular!)}
|
|
328
|
-
|
|
329
|
-
onClick={() => setItems([...items, newItem(root)])}
|
|
330
|
-
/>
|
|
331
|
-
)}
|
|
332
|
-
</>
|
|
333
|
-
}
|
|
321
|
+
actions={!isNew && <VersionsButton env={env} path={base.path} onPick={onVersion} />}
|
|
334
322
|
dirty={dirty}
|
|
335
323
|
saving={saving || deleting}
|
|
336
324
|
deleting={deleting}
|
|
@@ -424,6 +412,8 @@ function Form({
|
|
|
424
412
|
))}
|
|
425
413
|
</Group>
|
|
426
414
|
)}
|
|
415
|
+
{/* A single-file list: a new item in it. */}
|
|
416
|
+
{root && <CreateFab label={t.forms.addOne(root.singular!)} onClick={() => setItems([...items, newItem(root)])} />}
|
|
427
417
|
</FormFrame>
|
|
428
418
|
{picker}
|
|
429
419
|
</WrongField.Provider>
|
package/src/Deleted.tsx
CHANGED
|
@@ -38,7 +38,7 @@ const fileName = (path: string) => path.split('/').pop() ?? path;
|
|
|
38
38
|
|
|
39
39
|
export default function Deleted({ id, files, env }: { id: string; files: RepoFile[]; env: Env }) {
|
|
40
40
|
// The files from "Filer" (pictures, PDFs, videos, zip files), not a list's.
|
|
41
|
-
const pictures = id === '
|
|
41
|
+
const pictures = id === 'files';
|
|
42
42
|
const collection = pictures ? undefined : collections.find((c) => c.id === id && restorable(c));
|
|
43
43
|
const folder = pictures ? MEDIA : collection && collection.kind !== 'files' ? collection.folder : '';
|
|
44
44
|
const [deleted, setDeleted] = useState<DeletedFile[] | null>(null);
|
|
@@ -62,12 +62,12 @@ export default function Deleted({ id, files, env }: { id: string; files: RepoFil
|
|
|
62
62
|
if (!folder)
|
|
63
63
|
return (
|
|
64
64
|
<Screen title={t.app.notFound} back={{ href: '#', label: siteName }}>
|
|
65
|
-
<EmptyNote>{t.app.nothingAt(`
|
|
65
|
+
<EmptyNote>{t.app.nothingAt(`deleted/${id}`)}</EmptyNote>
|
|
66
66
|
</Screen>
|
|
67
67
|
);
|
|
68
68
|
|
|
69
69
|
const label = pictures ? t.history.files : collection!.label;
|
|
70
|
-
const back = pictures ? { href: '#
|
|
70
|
+
const back = pictures ? { href: '#files', label: t.common.parts.media } : { href: `#list/${id}`, label: collection!.label };
|
|
71
71
|
|
|
72
72
|
async function open(file: DeletedFile) {
|
|
73
73
|
if (!pictures) return restore(file);
|
package/src/EditorFooter.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#
|
|
1
|
+
// The editor's footer: one line under the content of every screen (Screen.tsx), with a link to "Licenser" (#licenses,
|
|
2
2
|
// Licenses.tsx). Exposes EditorFooter. Uses LinkButton from ui.tsx; the safe area at the bottom is Screen's main.
|
|
3
3
|
import { t } from './i18n';
|
|
4
4
|
import { LinkButton } from './ui';
|
|
@@ -6,7 +6,7 @@ import { LinkButton } from './ui';
|
|
|
6
6
|
export default function EditorFooter() {
|
|
7
7
|
return (
|
|
8
8
|
<footer className="mt-12 flex justify-center">
|
|
9
|
-
<LinkButton href="#
|
|
9
|
+
<LinkButton href="#licenses" kind="text" size="small">
|
|
10
10
|
{t.common.parts.licenses}
|
|
11
11
|
</LinkButton>
|
|
12
12
|
</footer>
|
package/src/EntryForm.tsx
CHANGED
|
@@ -99,7 +99,7 @@ export const newMarkdown = (collection: MarkdownCollection, off: string[] = []):
|
|
|
99
99
|
// The address of a saved page or post: a page's own `path`, or else /<file name>/ (packages/editor/site/content.ts).
|
|
100
100
|
// '' for a new one.
|
|
101
101
|
// Where the redirects are, as the menu has it: "Indhold" › "Videresendelser" › "Alle videresendelser".
|
|
102
|
-
const redirectsPath = () => [placeOf('
|
|
102
|
+
const redirectsPath = () => [placeOf('redirects'), t.common.parts.redirects, t.redirects.all].filter(Boolean).join(' › ');
|
|
103
103
|
|
|
104
104
|
function addressNow(collection: MarkdownCollection, file: RepoFile, data: Record<string, unknown>) {
|
|
105
105
|
if (!file.path) return '';
|
|
@@ -623,7 +623,7 @@ function Form({
|
|
|
623
623
|
<WrongField.Provider value={wrongField.field}>
|
|
624
624
|
<FormFrame
|
|
625
625
|
title={formTitle}
|
|
626
|
-
back={{ href: `#
|
|
626
|
+
back={{ href: `#list/${collection.id}`, label: collection.label }}
|
|
627
627
|
status={savedStatus({ savedAt, saving, deleting, isNew })}
|
|
628
628
|
view={isNew ? undefined : url}
|
|
629
629
|
dirty={dirty}
|
|
@@ -641,7 +641,7 @@ function Form({
|
|
|
641
641
|
<>
|
|
642
642
|
<FoldSection {...sectionProps('content')}>
|
|
643
643
|
{collection.fields.filter((f) => TOP.includes(f.name) && !STATUS.includes(f.name)).map(field)}
|
|
644
|
-
<div ref={textBox} className="scroll-mt-[calc(
|
|
644
|
+
<div ref={textBox} className="scroll-mt-[calc(1rem+env(safe-area-inset-top))]">
|
|
645
645
|
<span className="mb-1 block font-medium">{t.forms.text}</span>
|
|
646
646
|
<MarkdownEditor
|
|
647
647
|
ref={text}
|
package/src/FooterSettings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Footer" (#
|
|
1
|
+
// "Udseende" › "Footer" (#appearance/footer): the footer's text under the name, its two headings and its links (footer.json),
|
|
2
2
|
// on one screen laid out as a page's form (SettingsSections.tsx): a preview of the footer as the form has it, on a phone and
|
|
3
3
|
// a computer (partPreview.tsx), over the sections Text, Contact and Links. "Gem ændringer" saves the file in its own
|
|
4
4
|
// commit (useDataFiles.ts). Exposes FooterSettings (default). Uses fields.tsx and footerPreview.mjs.
|
|
@@ -30,7 +30,7 @@ const placeOf = (path: string) => places.find((p) => p.names.includes(path.split
|
|
|
30
30
|
// The class of a link in the footer (Footer.astro), for a footer whose page has no link to copy.
|
|
31
31
|
const THEME = { linkClass: 'md-footer-item' };
|
|
32
32
|
|
|
33
|
-
const back = { href: '#
|
|
33
|
+
const back = { href: '#appearance', label: t.common.parts.appearance };
|
|
34
34
|
|
|
35
35
|
export default function FooterSettings({ env }: { env: Env }) {
|
|
36
36
|
const layout = useSettingsSections(places);
|
package/src/FormFrame.tsx
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// of the screen with "Gem" (across the bar on a phone, at its end from medium), which is only there when something
|
|
4
4
|
// has been changed. Messages are shown by the app (Env.notify); the first "Gem" asks whether the editor may also send notifications.
|
|
5
5
|
// Leaving with changes that are not saved asks first, to save them or not (in the editor's dialog;
|
|
6
|
-
// closing or reloading the page asks in the browser's own). "Forhåndsvis" beside "Gem" and
|
|
6
|
+
// closing or reloading the page asks in the browser's own). "Forhåndsvis" beside "Gem" and, as a page action (Screen.tsx `pageActions`:
|
|
7
|
+
// the end of the title row from expanded, the top bar below it), shows the page with
|
|
7
8
|
// what is in the form, before it is saved (pagePreview.tsx).
|
|
8
9
|
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
9
10
|
import { ArrowSquareOut, Binoculars, Trash } from '@phosphor-icons/react';
|
|
@@ -177,9 +178,9 @@ export default function FormFrame<T extends string>({
|
|
|
177
178
|
drawer={drawer}
|
|
178
179
|
tabs={tabs}
|
|
179
180
|
back={{ guard: leaveTo, ...back }}
|
|
180
|
-
actions={
|
|
181
|
+
actions={actions}
|
|
182
|
+
pageActions={
|
|
181
183
|
<>
|
|
182
|
-
{actions}
|
|
183
184
|
{onPreview && <IconButton icon={Binoculars} label={t.preview.button} aria-haspopup="dialog" onClick={onPreview} />}
|
|
184
185
|
{view && <IconLink href={view} target="_blank" rel="noopener" label={t.app.viewOnSite} icon={ArrowSquareOut} />}
|
|
185
186
|
</>
|
package/src/HeaderSettings.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// "Udseende" › "Header" (#
|
|
1
|
+
// "Udseende" › "Header" (#appearance/header): what is in the header on every page, on one screen laid out as a page's form
|
|
2
2
|
// (SettingsSections.tsx): a preview of the header as the form has it, on a phone and a computer (partPreview.tsx), over the
|
|
3
3
|
// sections Main menu (menu.json), Mobile menu (mobile-menu.json), Action button and Menu button (header.json). "Gem
|
|
4
4
|
// ændringer" saves each changed file in its own commit (useDataFiles.ts). Exposes HeaderSettings (default). Uses
|
|
@@ -19,7 +19,7 @@ import type { Env } from './App';
|
|
|
19
19
|
import { t } from './i18n';
|
|
20
20
|
|
|
21
21
|
// The header's three files, by their collections (defaults/collections.ts), as the preview names them.
|
|
22
|
-
const FILES = { menu: 'menu', mobileMenu: '
|
|
22
|
+
const FILES = { menu: 'menu', mobileMenu: 'mobile-menu', header: 'header-button' } as const;
|
|
23
23
|
const parts: DataFilePart[] = Object.values(FILES).flatMap((id) => {
|
|
24
24
|
const collection = collections.find((c): c is FilesCollection => c.id === id && c.kind === 'files');
|
|
25
25
|
return collection ? [{ collection, dataFile: collection.files[0] }] : [];
|
|
@@ -51,7 +51,7 @@ const shapeOf = (values: Values) => ({
|
|
|
51
51
|
header: values[at(FILES.header)] ?? {},
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
const back = { href: '#
|
|
54
|
+
const back = { href: '#appearance', label: t.common.parts.appearance };
|
|
55
55
|
|
|
56
56
|
export default function HeaderSettings({ env }: { env: Env }) {
|
|
57
57
|
const layout = useSettingsSections(places);
|
package/src/Keywords.tsx
CHANGED
|
@@ -26,7 +26,7 @@ type Keyword = { word: string; primary: boolean; title: string; href: string; ad
|
|
|
26
26
|
type Counts = Record<string, { href: string; pages: number }>;
|
|
27
27
|
|
|
28
28
|
type Sort = 'az' | 'za' | 'most' | 'fewest';
|
|
29
|
-
const SORT_KEY = 'editor-sort-
|
|
29
|
+
const SORT_KEY = 'editor-sort-keywords';
|
|
30
30
|
function savedSort(): Sort {
|
|
31
31
|
try {
|
|
32
32
|
const s = localStorage.getItem(SORT_KEY);
|
|
@@ -85,11 +85,11 @@ function keywordsOf(files: RepoFile[]): Keyword[] {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
export default function Keywords({ files }: { files: RepoFile[] }) {
|
|
88
|
-
const [query, setQuery] = useKept('
|
|
88
|
+
const [query, setQuery] = useKept('keywords-search', '');
|
|
89
89
|
const [sort, setSort] = useState(savedSort);
|
|
90
|
-
const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('
|
|
91
|
-
const [linked, setLinked] = useKept<'' | 'with' | 'without'>('
|
|
92
|
-
const [kind, setKind] = useKept('
|
|
90
|
+
const [which, setWhich] = useKept<'' | 'primary' | 'secondary'>('keywords-kind', '');
|
|
91
|
+
const [linked, setLinked] = useKept<'' | 'with' | 'without'>('keywords-links', '');
|
|
92
|
+
const [kind, setKind] = useKept('keywords-page', '');
|
|
93
93
|
// undefined while they load; null when the site has none (not built since they came, or offline).
|
|
94
94
|
const [counts, setCounts] = useState<Counts | null>();
|
|
95
95
|
useEffect(() => {
|