@morphemecms/cms 0.8.0 → 0.8.1-next-20261105100001

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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # @morphemecms/cms
2
2
 
3
+ ## 0.8.1-next-20261105100001
4
+
5
+ ### Patch Changes
6
+
7
+ - 5c4fa60: Show the signed-in user and the site's domain at the top of the main drawer.
8
+ - c19ddf7: On a phone, hide the top bar's side panel button on screens that show a back arrow.
9
+
3
10
  ## 0.8.0
4
11
 
5
12
  ### Minor Changes
@@ -0,0 +1,5 @@
1
+ /** Up to two uppercase initials from `name` (its first and last word); the first letter of `login` when `name` is empty or null.
2
+ * @param {string | null | undefined} name @param {string} login */
3
+ export function initialsOf(name: string | null | undefined, login: string): string;
4
+ /** The address of a GitHub user's picture, `size` pixels square. @param {string} login @param {number} size */
5
+ export function avatarOf(login: string, size: number): string;
@@ -0,0 +1,16 @@
1
+ // The signed-in user's look in the editor: up to two initials from the name, and the address of the GitHub picture.
2
+ // Exposes initialsOf and avatarOf. No dependencies; AccountRow.tsx uses both.
3
+
4
+ /** Up to two uppercase initials from `name` (its first and last word); the first letter of `login` when `name` is empty or null.
5
+ * @param {string | null | undefined} name @param {string} login */
6
+ export function initialsOf(name, login) {
7
+ const words = (name ?? '').trim().split(/\s+/).filter(Boolean);
8
+ const letters = words.length > 1 ? [words[0], words[words.length - 1]] : words.slice(0, 1);
9
+ const initials = letters.map((w) => [...w][0]).join('');
10
+ return (initials || [...login][0] || '').toUpperCase();
11
+ }
12
+
13
+ /** The address of a GitHub user's picture, `size` pixels square. @param {string} login @param {number} size */
14
+ export function avatarOf(login, size) {
15
+ return `https://avatars.githubusercontent.com/${encodeURIComponent(login)}?s=${size}`;
16
+ }
@@ -0,0 +1,22 @@
1
+ // Tests for lib/account.mjs: the initials and the picture address of the signed-in user.
2
+ import test from 'node:test';
3
+ import assert from 'node:assert/strict';
4
+ import { avatarOf, initialsOf } from './account.mjs';
5
+
6
+ test('initialsOf takes the first letters of the name', () => {
7
+ assert.equal(initialsOf('Lars Adolfsen', 'larsadolfsen'), 'LA');
8
+ assert.equal(initialsOf('Lars', 'x'), 'L');
9
+ });
10
+
11
+ test('initialsOf takes the first and the last word', () => {
12
+ assert.equal(initialsOf('anna maria berg', 'x'), 'AB');
13
+ });
14
+
15
+ test('initialsOf falls back to the login', () => {
16
+ assert.equal(initialsOf(null, 'octocat'), 'O');
17
+ assert.equal(initialsOf(' ', 'octocat'), 'O');
18
+ });
19
+
20
+ test('avatarOf makes the GitHub picture address', () => {
21
+ assert.equal(avatarOf('octo cat', 80), 'https://avatars.githubusercontent.com/octo%20cat?s=80');
22
+ });
package/lib/csp.mjs CHANGED
@@ -23,9 +23,9 @@ export { allows, isSource, joined, KINDS, kindOf, sourcesProblems } from './cspS
23
23
  const VIDEOS = { frame: ['https://www.youtube-nocookie.com', 'https://player.vimeo.com'] };
24
24
 
25
25
  /** What the editor needs (GitHub it asks through the site's Worker, on its own address: github.ts): Vimeo's details of a video and the stills of the videos
26
- * (standard/video/view.ts), and what it makes in the browser (pictures, files) as blob: and data:. */
26
+ * (standard/video/view.ts), the signed-in user's GitHub picture (AccountRow.tsx), and what it makes in the browser (pictures, files) as blob: and data:. */
27
27
  const EDITOR = {
28
- img: ['blob:', 'https://i.ytimg.com', 'https://i.vimeocdn.com'],
28
+ img: ['blob:', 'https://i.ytimg.com', 'https://i.vimeocdn.com', 'https://avatars.githubusercontent.com'],
29
29
  connect: ['blob:', 'data:', 'https://vimeo.com'],
30
30
  };
31
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morphemecms/cms",
3
- "version": "0.8.0",
3
+ "version": "0.8.1-next-20261105100001",
4
4
  "description": "Morpheme CMS: the editor for a site built with Astro, its screens, its blocks and the rules a site gives it. See README.md.",
5
5
  "license": "MIT",
6
6
  "author": "Lars Adolfsen",
@@ -0,0 +1,35 @@
1
+ // The signed-in user as a list row: their GitHub picture in a round avatar (their initials until it loads or when it fails),
2
+ // their name and a line under it. Exposes AccountRow. Uses ListItem (ui.tsx) and lib/account.mjs; the main drawer's header
3
+ // (MainMenu.tsx) and the account screen (Library.tsx) show it.
4
+ import { useEffect, useState } from 'react';
5
+ import { avatarOf, initialsOf } from '../lib/account.mjs';
6
+ import type { Viewer } from './github';
7
+ import { ListItem, plainListClass } from './ui';
8
+
9
+ /** The user's avatar, name (`user.name`, or the login) and `supporting` under it, each on one line; a link when `href` is set. */
10
+ export function AccountRow({ user, supporting, href }: { user: Viewer; supporting?: string; href?: string }) {
11
+ const initials = initialsOf(user.name, user.login);
12
+ const src = avatarOf(user.login, 80);
13
+ const [loaded, setLoaded] = useState(false);
14
+ // The picture is loaded beforehand; the initials stand in until it is there and for good when it fails.
15
+ useEffect(() => {
16
+ setLoaded(false);
17
+ const image = new Image();
18
+ image.onload = () => setLoaded(true);
19
+ image.src = src;
20
+ return () => {
21
+ image.onload = null;
22
+ };
23
+ }, [src]);
24
+ return (
25
+ <ul className={plainListClass}>
26
+ <ListItem
27
+ href={href}
28
+ leadingAvatar={loaded ? { src } : initials}
29
+ headline={user.name ?? user.login}
30
+ supporting={supporting}
31
+ truncate
32
+ />
33
+ </ul>
34
+ );
35
+ }
package/src/App.tsx CHANGED
@@ -670,7 +670,7 @@ function Editor() {
670
670
  return (
671
671
  <>
672
672
  <TopBarLayout
673
- aside={<MainMenu route={route} files={files} />}
673
+ aside={<MainMenu route={route} files={files} user={user} />}
674
674
  above={
675
675
  files &&
676
676
  user &&
package/src/Library.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // The start screen of the editor (empty but for its title, status and FAB) and the
1
+ // The account screen (who is signed in, AccountRow) and the start screen of the editor (empty but for its title, status and FAB) and the
2
2
  // list of each part: blog posts newest first, the rest by name, prices and testimonials in their order
3
3
  // on the site (with arrows to move them), and a search. Lists that are not in an order of their own can
4
4
  // be sorted another way (a list of kinds, by its kinds too); the choice is kept in this browser. Pages
@@ -15,7 +15,6 @@ import {
15
15
  ClockCounterClockwise,
16
16
  CircleHalf,
17
17
  FileText,
18
- GithubLogo,
19
18
  GoogleLogo,
20
19
  List as ListIcon,
21
20
  ListBullets,
@@ -49,6 +48,7 @@ import {
49
48
  } from './site';
50
49
  import { readFields, splitFrontmatter } from './frontmatter.mjs';
51
50
  import { ConflictError, type RepoFile, type Viewer } from './github';
51
+ import { AccountRow } from './AccountRow';
52
52
  import Screen from './Screen';
53
53
  import {
54
54
  ImageCarousel,
@@ -718,15 +718,11 @@ export function Account({ user, files, onLogout }: { user: Viewer | null; files:
718
718
  // Titled with the name of the one logged in.
719
719
  <Screen title={user ? (user.name ?? user.login) : t.common.parts.account} back={{ href: '#', label: siteName }}>
720
720
  {/* Who is logged in, as plain text: nothing to tap. */}
721
- <div className="mb-8 flex items-center gap-3 px-4">
722
- <span className="flex size-12 shrink-0 items-center justify-center rounded-full text-muted">
723
- <GithubLogo size={28} />
724
- </span>
725
- <span className="min-w-0 flex-1">
726
- <span className="block truncate font-medium">{user ? (user.name ?? user.login) : ''}</span>
727
- {user && <span className="block truncate text-sm text-muted">{t.library.githubAccount(user.login)}</span>}
728
- </span>
729
- </div>
721
+ {user && (
722
+ <div className="mb-8">
723
+ <AccountRow user={user} supporting={t.library.githubAccount(user.login)} />
724
+ </div>
725
+ )}
730
726
  <section className="mb-8">
731
727
  <GroupTitle>{t.library.theme}</GroupTitle>
732
728
  <ThemePicker />
@@ -1,9 +1,11 @@
1
1
  // The main drawer and its menu button on #udseende/dele/editor (Parts.tsx): the modal drawer in its two levels (sections, and
2
2
  // a section's list with the back row and a fold-out), opened from a button, so it can be tried at any width, and the menu
3
- // button open and closed. Exposes MainDrawerSample. Uses MainDrawer and its parts from ui.tsx.
3
+ // button open and closed. The drawer's header is a sample user (AccountRow). Exposes MainDrawerSample. Uses MainDrawer and
4
+ // its parts from ui.tsx.
4
5
  import { useState } from 'react';
5
6
  import { icons } from './site';
6
7
  import { t } from './i18n';
8
+ import { AccountRow } from './AccountRow';
7
9
  import {
8
10
  Button,
9
11
  MainDrawer,
@@ -18,6 +20,8 @@ import {
18
20
 
19
21
  const { parts } = t.common;
20
22
 
23
+ const sampleUser = { login: 'octocat', name: 'Octo Cat', canSave: true };
24
+
21
25
  export function MainDrawerSample() {
22
26
  const [open, setOpen] = useState(false);
23
27
  const [section, setSection] = useState<string | null>(null);
@@ -38,6 +42,7 @@ export function MainDrawerSample() {
38
42
  return inList;
39
43
  }}
40
44
  variant="modal"
45
+ header={<AccountRow user={sampleUser} supporting="example.com" href="" />}
41
46
  >
42
47
  {section === null ? (
43
48
  <MainDrawerLevel>
package/src/MainMenu.tsx CHANGED
@@ -1,7 +1,8 @@
1
1
  // The editor's main menu (App.tsx renders it): the drawer (ui.tsx MainDrawer) in two levels. The first lists Home and the
2
2
  // sections; a section opens its list, under a back row, with the open screen marked and its fold-out folded out. Apps lists
3
3
  // Appstore and a row per app installed. From medium and wider the first level is also the collapsed navigation rail
4
- // (MainRailItem), whose sections open the drawer at their list. Exposes MainMenu. The tree and the open place are lib/menu.mjs
4
+ // (MainRailItem), whose sections open the drawer at their list. The drawer's header is the signed-in user (AccountRow), a link
5
+ // to the account. Exposes MainMenu. The tree and the open place are lib/menu.mjs
5
6
  // (menuTree, currentPlace); it only wires them to the drawer.
6
7
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
8
  import { config, header } from '@site/editor';
@@ -10,6 +11,7 @@ import { STORE_ROUTE } from './store';
10
11
  import { collectionOf } from './schema';
11
12
  import { apps, collections, groups, icons, lists, partScreens } from './site';
12
13
  import { partOf } from './Library';
14
+ import { AccountRow } from './AccountRow';
13
15
  import { childrenOf } from './menuChildren';
14
16
  import { t } from './i18n';
15
17
  import { useMenuOpen } from './menuOpen';
@@ -24,9 +26,9 @@ import {
24
26
  MainRailItem,
25
27
  MainRailSectionItem,
26
28
  } from './ui';
27
- import type { RepoFile } from './github';
29
+ import type { RepoFile, Viewer } from './github';
28
30
 
29
- export function MainMenu({ route, files }: { route: string; files: RepoFile[] | null }) {
31
+ export function MainMenu({ route, files, user }: { route: string; files: RepoFile[] | null; user: Viewer | null }) {
30
32
  const { open, setOpen } = useMenuOpen();
31
33
  const tree = useMemo(
32
34
  () =>
@@ -82,6 +84,7 @@ export function MainMenu({ route, files }: { route: string; files: RepoFile[] |
82
84
  open={open}
83
85
  onOpenChange={setOpen}
84
86
  onEscape={onEscape}
87
+ header={user && <AccountRow user={user} supporting={config.domain} href="#konto" />}
85
88
  rail={tree.map((entry) =>
86
89
  entry.href !== undefined ? (
87
90
  <MainRailItem key={entry.id} href={entry.href} icon={icons[entry.id]} label={entry.label} selected={route === ''} />
package/src/Screen.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  // A screen of the editor: a top bar like in an app, beside the navigation rail and the drawer (it is drawn into
2
2
  // topBarSlot.tsx's place, at the top of the content), above the content. Below medium, where there is no rail, the panel
3
- // button at its start opens and closes the main drawer (MainMenu.tsx, menuOpen.ts; from medium it is the rail's); at expanded and wider the logo and the site's name after it (one link to the start screen,
3
+ // button at its start opens and closes the main drawer (MainMenu.tsx, menuOpen.ts; from medium it is the rail's; not on a screen with a back arrow); at expanded and wider the logo and the site's name after it (one link to the start screen,
4
4
  // through `back.guard`), below expanded the back arrow, the title and the status as in an app (the logo only on the start
5
5
  // screen); at the end the actions, the notifications and the account, on every screen (on a phone the account only on the
6
6
  // start screen, `home`). At expanded and wider the way here as breadcrumbs (the screen above, when that is not the start
@@ -116,7 +116,7 @@ export default function Screen<T extends string>({
116
116
  const bar = (
117
117
  <TopAppBar>
118
118
  <div className="flex h-16 items-center gap-1 px-2 medium:gap-3 medium:px-6">
119
- {menuButtonInTopBar && (
119
+ {menuButtonInTopBar && !back && (
120
120
  <MenuButton
121
121
  label={menu.open ? t.library.closeMenu : t.library.openMenu}
122
122
  open={menu.open}
@@ -1,4 +1,4 @@
1
- // The logo and the site's name at the start of the top bar, after the menu button (Screen.tsx), and in the modal drawer after its close button (ui.tsx): one link to the start screen.
1
+ // The logo and the site's name at the start of the top bar, after the menu button (Screen.tsx): one link to the start screen.
2
2
  // Exposes SiteLogoLink; the link's click goes through `onClick` (Screen's `leave`, so the unsaved-changes warning shows).
3
3
  import type { MouseEventHandler } from 'react';
4
4
  import { siteName } from '@site/editor';
package/src/ui.tsx CHANGED
@@ -107,7 +107,6 @@ import {
107
107
  import { locale, t } from './i18n';
108
108
  import { InEditor, type ButtonKind, type IconButtonKind } from './standard/view';
109
109
  import { SvgFile } from './icon';
110
- import { SiteLogoLink } from './SiteLogoLink';
111
110
  import { countries, countryName, dialCodes, flagOf, regionOf } from './countries';
112
111
  import { byText } from './schema';
113
112
  import { contrast } from '../lib/colors';
@@ -2206,6 +2205,9 @@ export function ChoiceRow({
2206
2205
  * (Material 3 Expressive's), each row on its own surface. */
2207
2206
  export const listClass = siteListClasses('segmented');
2208
2207
 
2208
+ /** The classes of a list without surfaces (the theme's standard list): a row or two that stand on the page, AccountRow. */
2209
+ export const plainListClass = siteListClasses('standard');
2210
+
2209
2211
  /** The small heading over a Group, and over what stands as one (the themes on Konto): in Material 3's title small, as
2210
2212
  * written (not in capitals, Material 2's subheader), muted as a group's heading in a sheet (SheetGroupTitle). */
2211
2213
  export function GroupTitle({ children }: { children: ReactNode }) {
@@ -2665,7 +2667,8 @@ export function SectionPane({ caption, title, children }: { caption: string; tit
2665
2667
  }
2666
2668
 
2667
2669
  /** The editor's main navigation (Material 3's navigation drawer, the theme's, in its design): the site's parts in sections
2668
- * (`MainDrawerSection`, `MainDrawerItem`), at the full height of the window, with the menu button at the top. From medium
2670
+ * (`MainDrawerSection`, `MainDrawerItem`), at the full height of the window, with a header row at the top: `header` (the
2671
+ * signed-in user, AccountRow) at the start and the menu button at the end. From medium
2669
2672
  * and wider a navigation rail (`rail`: its destinations, `MainRailItem` and `MainRailSectionItem`) stands in its place while
2670
2673
  * the drawer is closed; its menu button, or a section in it, opens the drawer (`onOpenChange`). The drawer is standard at
2671
2674
  * expanded and wider, beside the content (`open` shows and hides it, and the rail the other way round); modal below, over a
@@ -2680,6 +2683,7 @@ export function MainDrawer({
2680
2683
  onEscape,
2681
2684
  variant,
2682
2685
  rail,
2686
+ header,
2683
2687
  children,
2684
2688
  }: {
2685
2689
  label: string;
@@ -2688,6 +2692,7 @@ export function MainDrawer({
2688
2692
  onEscape?: () => boolean;
2689
2693
  variant?: 'modal' | 'standard';
2690
2694
  rail?: ReactNode;
2695
+ header?: ReactNode;
2691
2696
  children: ReactNode;
2692
2697
  }) {
2693
2698
  const layout = useMenuLayout();
@@ -2701,11 +2706,11 @@ export function MainDrawer({
2701
2706
  </MainRail>
2702
2707
  )}
2703
2708
  {shown === 'standard' ? (
2704
- <StandardMainDrawer label={label} open={open} onOpenChange={onOpenChange} onEscape={onEscape}>
2709
+ <StandardMainDrawer label={label} open={open} onOpenChange={onOpenChange} onEscape={onEscape} header={header}>
2705
2710
  {children}
2706
2711
  </StandardMainDrawer>
2707
2712
  ) : (
2708
- <ModalMainDrawer label={label} open={open} onOpenChange={onOpenChange} onEscape={onEscape}>
2713
+ <ModalMainDrawer label={label} open={open} onOpenChange={onOpenChange} onEscape={onEscape} header={header}>
2709
2714
  {children}
2710
2715
  </ModalMainDrawer>
2711
2716
  )}
@@ -2731,13 +2736,13 @@ function MainRail({ label, onOpen, children }: { label: string; onOpen: () => vo
2731
2736
  );
2732
2737
  }
2733
2738
 
2734
- // The menu button at the top of either drawer: its icon over the items' icons, level with the top bar's logo; it closes the drawer.
2735
- // The modal drawer (`logo`) shows the site's logo after the button.
2736
- function MainDrawerMenuButton({ onClick, logo }: { onClick: () => void; logo?: boolean }) {
2739
+ // The header row at the top of either drawer: `header` (the signed-in user, MainMenu.tsx) at the start and the menu button at
2740
+ // the end, which closes the drawer and stays level with the top bar's menu button.
2741
+ function MainDrawerMenuButton({ onClick, header }: { onClick: () => void; header?: ReactNode }) {
2737
2742
  return (
2738
- <div className="ms-1 mt-2 mb-7 flex items-center gap-1 self-start">
2743
+ <div className="mt-2 mb-7 flex items-center gap-1 ps-2 pe-1">
2744
+ <div className="min-w-0 flex-1">{header}</div>
2739
2745
  <MenuButton label={t.library.closeMenu} open onClick={onClick} />
2740
- {logo && <SiteLogoLink />}
2741
2746
  </div>
2742
2747
  );
2743
2748
  }
@@ -2754,12 +2759,14 @@ function StandardMainDrawer({
2754
2759
  open,
2755
2760
  onOpenChange,
2756
2761
  onEscape,
2762
+ header,
2757
2763
  children,
2758
2764
  }: {
2759
2765
  label: string;
2760
2766
  open: boolean;
2761
2767
  onOpenChange: (open: boolean) => void;
2762
2768
  onEscape?: () => boolean;
2769
+ header?: ReactNode;
2763
2770
  children: ReactNode;
2764
2771
  }) {
2765
2772
  if (!open) return null;
@@ -2772,7 +2779,7 @@ function StandardMainDrawer({
2772
2779
  if (e.key === 'Escape' && onEscape?.()) e.preventDefault();
2773
2780
  }}
2774
2781
  >
2775
- <MainDrawerMenuButton onClick={() => onOpenChange(false)} />
2782
+ <MainDrawerMenuButton onClick={() => onOpenChange(false)} header={header} />
2776
2783
  <MainDrawerBody label={label}>{children}</MainDrawerBody>
2777
2784
  </SiteNavigationDrawer>
2778
2785
  </aside>
@@ -2786,12 +2793,14 @@ function ModalMainDrawer({
2786
2793
  open,
2787
2794
  onOpenChange,
2788
2795
  onEscape,
2796
+ header,
2789
2797
  children,
2790
2798
  }: {
2791
2799
  label: string;
2792
2800
  open: boolean;
2793
2801
  onOpenChange: (open: boolean) => void;
2794
2802
  onEscape?: () => boolean;
2803
+ header?: ReactNode;
2795
2804
  children: ReactNode;
2796
2805
  }) {
2797
2806
  const dialog = useRef<HTMLDialogElement>(null);
@@ -2838,7 +2847,7 @@ function ModalMainDrawer({
2838
2847
  }
2839
2848
  }}
2840
2849
  >
2841
- <MainDrawerMenuButton onClick={() => onOpenChange(false)} logo />
2850
+ <MainDrawerMenuButton onClick={() => onOpenChange(false)} header={header} />
2842
2851
  <MainDrawerBody label={label}>{children}</MainDrawerBody>
2843
2852
  </SiteNavigationDrawer>
2844
2853
  </dialog>