@graview/primitives 0.1.16 → 0.1.17

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.
Files changed (60) hide show
  1. package/README.md +44 -2
  2. package/dist/app-bar.d.ts +41 -19
  3. package/dist/app-bar.d.ts.map +1 -1
  4. package/dist/app-bar.js +149 -121
  5. package/dist/app-bar.js.map +1 -1
  6. package/dist/bar-panes.d.ts.map +1 -1
  7. package/dist/bar-panes.js +3 -27
  8. package/dist/bar-panes.js.map +1 -1
  9. package/dist/choice.d.ts +9 -0
  10. package/dist/choice.d.ts.map +1 -0
  11. package/dist/choice.js +21 -0
  12. package/dist/choice.js.map +1 -0
  13. package/dist/declared-lens-doors.d.ts.map +1 -1
  14. package/dist/declared-lens-doors.js +8 -1
  15. package/dist/declared-lens-doors.js.map +1 -1
  16. package/dist/frame.d.ts +2 -2
  17. package/dist/frame.d.ts.map +1 -1
  18. package/dist/frame.js +1 -1
  19. package/dist/frame.js.map +1 -1
  20. package/dist/graview-mark.d.ts +21 -0
  21. package/dist/graview-mark.d.ts.map +1 -0
  22. package/dist/graview-mark.js +24 -0
  23. package/dist/graview-mark.js.map +1 -0
  24. package/dist/index.d.ts +4 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +3 -3
  27. package/dist/index.js.map +1 -1
  28. package/dist/ladder.d.ts.map +1 -1
  29. package/dist/ladder.js +4 -10
  30. package/dist/ladder.js.map +1 -1
  31. package/dist/pages.d.ts +3 -3
  32. package/dist/pages.d.ts.map +1 -1
  33. package/dist/pages.js +2 -2
  34. package/dist/pages.js.map +1 -1
  35. package/dist/scene-css.js +2 -2
  36. package/dist/scene.d.ts +0 -1
  37. package/dist/scene.d.ts.map +1 -1
  38. package/dist/scene.js +0 -1
  39. package/dist/scene.js.map +1 -1
  40. package/dist/seats.d.ts.map +1 -1
  41. package/dist/seats.js +2 -11
  42. package/dist/seats.js.map +1 -1
  43. package/dist/shell.d.ts.map +1 -1
  44. package/dist/shell.js +1 -2
  45. package/dist/shell.js.map +1 -1
  46. package/dist/spec-views.d.ts +8 -0
  47. package/dist/spec-views.d.ts.map +1 -1
  48. package/dist/spec-views.js +17 -3
  49. package/dist/spec-views.js.map +1 -1
  50. package/dist/theme.d.ts.map +1 -1
  51. package/dist/theme.js +13 -7
  52. package/dist/theme.js.map +1 -1
  53. package/fonts/OFL-Montserrat.txt +93 -0
  54. package/fonts/montserrat-latin.woff2 +0 -0
  55. package/fonts/montserrat.css +23 -0
  56. package/package.json +13 -8
  57. package/dist/home-landing.d.ts +0 -18
  58. package/dist/home-landing.d.ts.map +0 -1
  59. package/dist/home-landing.js +0 -59
  60. package/dist/home-landing.js.map +0 -1
package/README.md CHANGED
@@ -40,8 +40,10 @@ the schema, the invariants and the op log.
40
40
 
41
41
  **The app, said once** — `AppBar`: one bar over every face (FR-131) — the
42
42
  brand's mark (`AppMark`, drawn as given, never an SVG that could act) and
43
- the app's name as the page's heading, the app's places as tabs (`barPlaces`,
44
- the overview among them, FR-132), and three tools of one size: the Find a
43
+ the app's name as the page's heading, the switch between the scene and the
44
+ pages (`faces`, FR-137), on Pages the place you are on as one control that
45
+ opens every place (`barPlaces`: the home, the Lists, the Pictures, FR-138),
46
+ and three tools of one size: the Find a
45
47
  face puts in it (`useBarFind`), `Standing` and `Profile`. `AppTitle` draws
46
48
  the mark, the name and the line under it elsewhere; `useFavicon` is for a
47
49
  face that owns the page.
@@ -54,3 +56,43 @@ a view spec is drawn with, which each face that draws a view draws beside it;
54
56
  and `sceneCss`, the rules only the scene draws (the districts from altitude,
55
57
  the plots, the village, the billboards), which an embed's scene face draws
56
58
  beside it so a page on the pages face carries none of them.
59
+
60
+ ## Design language
61
+
62
+ Three layers, and a component reads only the middle one:
63
+
64
+ | Layer | Where | What it holds |
65
+ |---|---|---|
66
+ | The identity | @graview/core (the design kit, revision 03) | En Dash navy #001769 and turquoise #00E5B9, paper #F7F7F2, ink #18213A, muted #586174; Montserrat at 550, 450 and 600; the shared-plane symbol |
67
+ | The semantic tokens | `themeCss` writes them as custom properties | surfaces (`--graview-ground`, `-panel`, `-panel-muted`, `-panel-warning`, `-bar`, `-float`), text (`--graview-ink`, `-ink-muted`, `-ink-faint`), interaction (`--graview-accent`, `-accent-dim`, `-accent-ink`, `-edge`, `-edge-bright`), state (`--graview-warn`, `-good`, `-bad`), type (`--graview-font-body`, `-display`, `-mono`, `--graview-weight-display`, `-body`, `-label`), shape (`--graview-radius`, `-radius-sm`, `-pad`, `-pad-sm`, `-gap`), depth (`--graview-lift-low`, `-lift-high`, `--graview-layer-*`) and the kit (`--graview-kit-*`) |
68
+ | The app's overrides | the app's brand | name, logo, page icon, both schemes' tokens, faces and weights, radius and density, a hue per kind, the kit |
69
+
70
+ The shipped schemes are built on the identity: paper, reading ink and navy in
71
+ the light, a deep navy ground with the navy's hue made light in the dark.
72
+ Neither paints a wash or a glow; hover firms an edge rather than glowing.
73
+ Turquoise is not a token at all: it is the symbol's point, a fill that is
74
+ never text (1.5:1 on paper; navy reads on it at 9.7:1), so nothing the
75
+ framework draws depends on it. An app that declares its own brand keeps its
76
+ own colors, faces and logo, and the framework never puts Graview's mark in it.
77
+
78
+ How the scene reads against any district's color: a kind's hue is the
79
+ brand's (`accents`) or a stable hash, and only the hue: the lighting of a
80
+ block's three faces and the plot under it is the scene's, so a block reads
81
+ as a block on both grounds. Selection and the keyboard's place are drawn in
82
+ the accent, as an outline set off from the block, never as a change of the
83
+ block's own hue, so a selected block in a navy-ish district still reads as
84
+ selected. A relation is told from another by its pattern, its end cap and its
85
+ width as well as its color, and every district, block and relation carries
86
+ its name in words, so nothing is said by color alone. Disabled is a dimmed
87
+ control that cannot be pressed; empty, loading and refused are said in
88
+ sentences.
89
+
90
+ The words are set in Montserrat when the host has it, and in the reader's
91
+ system sans otherwise: the framework names the face and never fetches it.
92
+ A host that wants it imports the optional copy once —
93
+ `import "@graview/primitives/montserrat.css"`, 32 KB of woff2, weights
94
+ 400–700, Latin, under the SIL Open Font License (beside it in the package).
95
+ `GraviewMark` is the symbol for a host drawing Graview's own identity — a
96
+ signature, a hosting service's chrome — inline in the ink it stands in, the
97
+ point in `--graview-mark-point` when set, decorative unless given a `title`,
98
+ and the optical micro cut under 28 px.
package/dist/app-bar.d.ts CHANGED
@@ -1,20 +1,27 @@
1
1
  import { type AnySchema, type Brand, type PagesArrangement, type Place, type Principal, type Store } from "@graview/core";
2
2
  import { type CSSProperties, type ReactNode } from "react";
3
- /** One of the app's places on the bar: what it is called, and where it is on the routed face. */
3
+ /** Which of the places a place is: the home, a kind's list, or a picture. */
4
+ export type BarPlaceGroup = "home" | "lists" | "pictures";
5
+ /** One of the app's places, as the bar's place control lists it: what it is called, and where it is on the routed face. */
4
6
  export interface BarPlace {
5
7
  readonly key: string;
6
8
  readonly label: string;
7
- /** Its path on the routed face — `/places/overview` for the scene. */
9
+ /** Its path on the routed face. */
8
10
  readonly path: string;
11
+ readonly group: BarPlaceGroup;
12
+ /** The kind it is a list or a picture of, for its mark. */
13
+ readonly kind?: string;
9
14
  }
10
- /** The key the scene's place has on the bar. */
11
- export declare const OVERVIEW_KEY = "overview";
15
+ /** The key the home's place has (FR-136): the app's front page, first in the list, at the app's own address. */
16
+ export declare const HOME_KEY = "home";
17
+ /** The home's address on the routed face: the app's own. */
18
+ export declare const HOME_PATH = "/";
12
19
  /**
13
- * THE APP'S PLACES, IN THE BAR'S ORDER: the overview (when the face can draw
14
- * the scene), each named picture, each kind's list in the declaration's
15
- * order, and how the kinds connect, when they do. What a seat may not see
16
- * is not offered: a kind kept from it, a disabled module's kind, a picture
17
- * over either.
20
+ * THE APP'S PLACES, IN THE LIST'S ORDER (FR-138): the home; each kind's list
21
+ * in the declaration's order; each named picture, then how the kinds
22
+ * connect, when they do. What a seat may not see is not offered: a kind
23
+ * kept from it, a disabled module's kind, a picture over either. The scene
24
+ * is not a place here: it is the switch's (FR-137).
18
25
  */
19
26
  export declare function barPlaces(input: {
20
27
  readonly store: Store<AnySchema>;
@@ -23,10 +30,8 @@ export declare function barPlaces(input: {
23
30
  places(): readonly Place[];
24
31
  arrangement?(): PagesArrangement | undefined;
25
32
  } | undefined;
26
- /** Whether the scene is one of the places here. */
27
- readonly overview: boolean;
28
33
  }): readonly BarPlace[];
29
- /** Which of the places a path on the routed face is: its own, a record's kind's, or none (the home, Find, the problems). */
34
+ /** Which of the places a path on the routed face is: its own, a record's kind's, or none (Find, the problems). */
30
35
  export declare function barPlaceAt(places: readonly BarPlace[], path: string): string | null;
31
36
  /**
32
37
  * WHERE A FACE PUTS ITS FIND BOX (FR-131). The bar draws the place; the
@@ -35,7 +40,7 @@ export declare function barPlaceAt(places: readonly BarPlace[], path: string): s
35
40
  */
36
41
  export interface BarFind {
37
42
  readonly slot: HTMLElement;
38
- /** A phone's bar: the box opens over the bar's first row when it is asked for. */
43
+ /** A phone's bar: the box opens over the bar's row when it is asked for. */
39
44
  readonly compact: boolean;
40
45
  }
41
46
  export declare const BarFindContext: import("react").Context<BarFind | null>;
@@ -43,22 +48,37 @@ export declare const BarFindContext: import("react").Context<BarFind | null>;
43
48
  export declare const useBarFind: () => BarFind | null;
44
49
  /** How a place is reached: a link when it has an address, a press when the bar decides. */
45
50
  export interface BarGo {
46
- /** The address a place's link carries; none, and the tab is a button. */
51
+ /** The address a place's link carries; none, and the entry is a button. */
47
52
  readonly href?: (path: string) => string | undefined;
48
53
  /** Goes to a place; a link's ordinary press is handed here rather than followed. */
49
54
  readonly go?: (place: BarPlace) => void;
50
55
  }
56
+ /** One of the switch's two faces: what it is called, whether it is drawn, and the way to it. */
57
+ export interface BarFace {
58
+ readonly label: string;
59
+ readonly current: boolean;
60
+ readonly go: () => void;
61
+ }
62
+ /** THE SWITCH (FR-137): the scene and the pages, the app's two faces. */
63
+ export interface BarFaces {
64
+ readonly scene: BarFace;
65
+ readonly pages: BarFace;
66
+ }
51
67
  /** One size for every tool on the bar (FR-131). */
52
68
  export declare const TOOL = 30;
69
+ /** The bar's one row, its rule under it included, on a desk and a phone alike (FR-138). */
70
+ export declare const BAR_HEIGHT = 48;
53
71
  /** A tool's own box: one size, square, its name in words for whoever cannot see the mark. */
54
72
  export declare const toolStyle: CSSProperties;
55
73
  /**
56
74
  * THE BAR. The app at the left — its mark, its name as the page's heading,
57
- * the way home — its places in the middle, and the tools at the right: the
58
- * Find a face puts in it, then `tools` (the standing and the person). One
59
- * row at a desk; on a phone the places take a second row of their own.
75
+ * the way home — then the switch between the scene and the pages (FR-137),
76
+ * then, on Pages, the place you are on (FR-138), and the tools at the
77
+ * right: the Find a face puts in it, then `tools` (the standing and the
78
+ * person). One row of one height on a desk and a phone; on a phone the
79
+ * place is the page's first line, under the bar.
60
80
  */
61
- export declare function AppBar({ brand, name, heading, home, places, current, reach, tools, find, description, findBox, onFind, }: {
81
+ export declare function AppBar({ brand, name, heading, home, faces, places, current, reach, tools, find, description, findBox, onFind, }: {
62
82
  readonly brand: Brand | undefined;
63
83
  readonly name: string;
64
84
  /** The level the app's name is said at: 1 when the page is the app's, `false` when the host's own heading already says it. */
@@ -69,8 +89,10 @@ export declare function AppBar({ brand, name, heading, home, places, current, re
69
89
  readonly go?: (() => void) | undefined;
70
90
  readonly current: boolean;
71
91
  };
92
+ /** The switch between the scene and the pages; none where the face cannot draw the scene. */
93
+ readonly faces?: BarFaces | undefined;
72
94
  readonly places: readonly BarPlace[];
73
- /** The key of the place the reader is on; null at the home, at Find, at the problems. */
95
+ /** The key of the place the reader is on; null at Find, at the problems. */
74
96
  readonly current: string | null;
75
97
  readonly reach: BarGo;
76
98
  /** The standing and the person, after Find. */
@@ -1 +1 @@
1
- {"version":3,"file":"app-bar.d.ts","sourceRoot":"","sources":["../src/app-bar.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app-bar.d.ts","sourceRoot":"","sources":["../src/app-bar.tsx"],"names":[],"mappings":"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"}
package/dist/app-bar.js CHANGED
@@ -1,17 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { OVERVIEW_PATH, OVERVIEW_SLUG, orderKinds, overviewTitle, placeSlug } from "@graview/core";
2
+ import { hueFor, orderKinds, OVERVIEW_SLUG, placeSlug } from "@graview/core";
3
3
  import { POPOVER_STYLE, usePopover } from "@graview/react/provider";
4
4
  import { createContext, useCallback, useContext, useLayoutEffect, useRef, useState } from "react";
5
5
  import { AppMark } from "./app-title.js";
6
- /** The key the scene's place has on the bar. */
7
- export const OVERVIEW_KEY = OVERVIEW_SLUG;
6
+ /** The key the home's place has (FR-136): the app's front page, first in the list, at the app's own address. */
7
+ export const HOME_KEY = "home";
8
+ /** The home's address on the routed face: the app's own. */
9
+ export const HOME_PATH = "/";
8
10
  const upper = (word) => word.charAt(0).toUpperCase() + word.slice(1);
9
11
  /**
10
- * THE APP'S PLACES, IN THE BAR'S ORDER: the overview (when the face can draw
11
- * the scene), each named picture, each kind's list in the declaration's
12
- * order, and how the kinds connect, when they do. What a seat may not see
13
- * is not offered: a kind kept from it, a disabled module's kind, a picture
14
- * over either.
12
+ * THE APP'S PLACES, IN THE LIST'S ORDER (FR-138): the home; each kind's list
13
+ * in the declaration's order; each named picture, then how the kinds
14
+ * connect, when they do. What a seat may not see is not offered: a kind
15
+ * kept from it, a disabled module's kind, a picture over either. The scene
16
+ * is not a place here: it is the switch's (FR-137).
15
17
  */
16
18
  export function barPlaces(input) {
17
19
  const { store, principal, views } = input;
@@ -19,27 +21,26 @@ export function barPlaces(input) {
19
21
  const live = store.schema.kinds.filter((kind) => !store.modules.disabledKinds.has(kind) && !kept.has(kind));
20
22
  const arrangement = views?.arrangement?.();
21
23
  const plural = (kind) => store.schema.tryDefinition(kind)?.plural ?? `${kind}s`;
22
- const out = [];
23
- if (input.overview)
24
- out.push({ key: OVERVIEW_KEY, label: overviewTitle(arrangement), path: OVERVIEW_PATH });
24
+ const out = [{ key: HOME_KEY, label: "Home", path: HOME_PATH, group: "home" }];
25
+ for (const kind of orderKinds(live, arrangement?.order))
26
+ out.push({ key: `kind:${kind}`, label: upper(plural(kind)), path: `/${placeSlug(plural(kind))}`, group: "lists", kind });
25
27
  const all = views?.places() ?? [];
26
28
  for (const place of all) {
29
+ // A place at the scene's address is not offered: the address is the scene's (FR-132).
27
30
  if (!live.includes(place.kind) || place.as === OVERVIEW_SLUG)
28
31
  continue;
29
32
  // Two kinds' pictures of one name say whose with `?of=` (as the routed face addresses them).
30
33
  const shared = all.some((other) => other.as === place.as && other.kind !== place.kind);
31
34
  const path = `/places/${encodeURIComponent(place.as)}${shared ? `?of=${placeSlug(plural(place.kind))}` : ""}`;
32
35
  if (!out.some((held) => held.path === path))
33
- out.push({ key: `place:${place.kind}:${place.as}`, label: place.title, path });
36
+ out.push({ key: `place:${place.kind}:${place.as}`, label: place.title, path, group: "pictures", kind: place.kind });
34
37
  }
35
- for (const kind of orderKinds(live, arrangement?.order))
36
- out.push({ key: `kind:${kind}`, label: upper(plural(kind)), path: `/${placeSlug(plural(kind))}` });
37
38
  const connects = live.some((kind) => Object.keys((store.schema.tryDefinition(kind)?.edges ?? {})).length > 0);
38
39
  if (connects)
39
- out.push({ key: "connections", label: "Connections", path: "/map" });
40
+ out.push({ key: "connections", label: "Connections", path: "/map", group: "pictures" });
40
41
  return out;
41
42
  }
42
- /** Which of the places a path on the routed face is: its own, a record's kind's, or none (the home, Find, the problems). */
43
+ /** Which of the places a path on the routed face is: its own, a record's kind's, or none (Find, the problems). */
43
44
  export function barPlaceAt(places, path) {
44
45
  const [pathname = "/", search = ""] = path.split("?");
45
46
  const exact = places.find((place) => place.path === (search ? `${pathname}?${search}` : pathname)) ?? places.find((place) => place.path === pathname);
@@ -47,43 +48,57 @@ export function barPlaceAt(places, path) {
47
48
  return exact.key;
48
49
  const first = `/${pathname.split("/").filter(Boolean)[0] ?? ""}`;
49
50
  // A record's page is under its kind's list.
50
- return places.find((place) => place.key.startsWith("kind:") && place.path === first)?.key ?? null;
51
+ return places.find((place) => place.group === "lists" && place.path === first)?.key ?? null;
51
52
  }
52
53
  export const BarFindContext = createContext(null);
53
54
  /** The bar's place for a Find box, when a bar above this face has one. */
54
55
  export const useBarFind = () => useContext(BarFindContext);
55
56
  /** One size for every tool on the bar (FR-131). */
56
57
  export const TOOL = 30;
58
+ /** The bar's one row, its rule under it included, on a desk and a phone alike (FR-138). */
59
+ export const BAR_HEIGHT = 48;
57
60
  const BAR_CSS = `
58
- .graview-bar{container-type:inline-size;flex:0 0 auto;position:relative;background:var(--graview-bar);border-bottom:1px solid var(--graview-edge);color:var(--graview-ink);font-family:var(--graview-font-body,system-ui)}
59
- .graview-bar-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;grid-template-areas:"app places tools";align-items:stretch;column-gap:20px;padding:0 16px;min-height:48px}
60
- .graview-bar-app{grid-area:app;display:flex;align-items:center;min-width:0}
61
- .graview-bar-name{margin:0;min-width:0;display:flex;font-family:var(--graview-font-display,var(--graview-font-body,system-ui));font-size:.9375rem;font-weight:600;line-height:1.3;letter-spacing:-.005em}
62
- .graview-bar-home{display:inline-flex;align-items:center;gap:8px;min-width:0;min-height:30px;color:var(--graview-ink);text-decoration:none;font:inherit;letter-spacing:inherit;overflow-wrap:anywhere;margin:0;padding:0;border:0;background:none;box-shadow:none;cursor:pointer;text-align:left}
63
- .graview-bar-more{display:grid;min-width:200px;max-width:calc(100vw - 32px);margin:0;padding:4px;list-style:none;border-radius:10px;border:1px solid var(--graview-edge);background:var(--graview-float);box-shadow:var(--graview-lift-high)}
64
- .graview-bar-more[hidden]{display:none}
65
- .graview-bar-item{display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-left:2px solid transparent;border-radius:7px;background:none;box-shadow:none;font:inherit;font-size:.875rem;text-align:left;text-decoration:none;color:var(--graview-ink);cursor:pointer}
66
- .graview-bar-item[aria-current="page"]{font-weight:600;border-left-color:var(--graview-accent)}
67
- .graview-bar-places{grid-area:places;display:flex;align-items:stretch;min-width:0}
68
- .graview-bar-tools{grid-area:tools;display:flex;align-items:center;gap:8px;min-width:0}
69
- .graview-bar-find{display:flex;align-items:center;width:15rem;min-width:0}
70
- .graview-bar-find-open{display:none;align-items:center;justify-content:center}
71
- .graview-bar-tab{display:inline-flex;align-items:center;gap:4px;padding:0 10px;margin:0;border:0;border-bottom:2px solid transparent;border-radius:0;background:none;box-shadow:none;font:inherit;font-size:.875rem;line-height:1.2;white-space:nowrap;color:var(--graview-ink-muted);text-decoration:none;cursor:pointer;flex:0 0 auto}
72
- .graview-bar-tab:hover{color:var(--graview-ink)}
73
- .graview-bar-tab[aria-current="page"]{color:var(--graview-ink);border-bottom-color:var(--graview-accent);font-weight:600}
74
- .graview-bar-tab:focus-visible{outline:2px solid var(--graview-accent);outline-offset:-4px}
75
- .graview-bar-measure{position:absolute;visibility:hidden;pointer-events:none;display:flex;white-space:nowrap;height:0;overflow:hidden}
61
+ .graview-bar{display:block;margin:0;padding:0;container-type:inline-size;flex:0 0 auto;position:relative;background:var(--graview-bar);border-bottom:1px solid var(--graview-edge);color:var(--graview-ink);font-family:var(--graview-font-body,system-ui)}
62
+ .graview-bar-row{display:flex;flex-wrap:nowrap;align-items:center;gap:16px;height:${BAR_HEIGHT - 1}px;margin:0;padding:0 16px}
63
+ .graview-bar-home,.graview-bar-face,.graview-bar-place,.graview-bar-item{display:inline-flex;align-items:center;box-sizing:border-box;min-width:0;margin:0;border:0;background:none;box-shadow:none;font:inherit;letter-spacing:normal;text-transform:none;text-decoration:none;color:var(--graview-ink);cursor:pointer;text-align:left}
64
+ .graview-bar-app{display:flex;flex:0 1 auto;min-width:0;max-width:34%}
65
+ .graview-bar-name{margin:0;min-width:0;display:flex;font-family:var(--graview-font-display,var(--graview-font-body,system-ui));font-size:.9375rem;font-weight:600;line-height:1.25}
66
+ .graview-bar-home{gap:8px;min-height:30px;padding:0;letter-spacing:-.005em;overflow-wrap:anywhere}
67
+ .graview-bar-faces{display:inline-flex;flex:0 0 auto;box-sizing:border-box;height:30px;margin:0;padding:2px;gap:2px;border:1px solid var(--graview-edge);border-radius:8px}
68
+ .graview-bar-face{gap:6px;padding:0 10px;border-radius:6px;font-size:.8125rem;white-space:nowrap;color:var(--graview-ink-muted)}
69
+ .graview-bar-face span{min-width:0;max-width:11em;overflow:hidden;text-overflow:ellipsis}
70
+ .graview-bar-face[aria-pressed=true]{color:var(--graview-ink);font-weight:600;background:color-mix(in srgb,var(--graview-accent) 16%,transparent)}
71
+ .graview-bar-mid{display:flex;flex:1 1 auto;min-width:0}
72
+ .graview-bar-place-at{position:relative;display:inline-flex;min-width:0;max-width:100%}
73
+ .graview-bar-place{gap:6px;height:30px;max-width:100%;padding:0 8px;border:1px solid transparent;border-radius:8px;font-size:.875rem;font-weight:600}
74
+ .graview-bar-face:hover,.graview-bar-place:hover{border-color:var(--graview-edge);color:var(--graview-ink)}
75
+ .graview-bar-place-words{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
76
+ .graview-bar-list{display:grid;width:min(320px,calc(100vw - 24px));max-height:min(70vh,560px);overflow:auto;margin:0;padding:6px;border-radius:10px;border:1px solid var(--graview-edge);background:var(--graview-float);box-shadow:var(--graview-lift-high);color:var(--graview-ink)}
77
+ .graview-bar-list[hidden]{display:none}
78
+ .graview-bar-list p{margin:8px 8px 2px;font-size:.6875rem;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--graview-ink-muted)}
79
+ .graview-bar-list ul,.graview-bar-list li{display:grid;margin:0;padding:0;list-style:none}
80
+ .graview-bar-item{align-items:flex-start;gap:10px;width:100%;padding:7px 8px;border-radius:7px;font-size:.875rem;line-height:1.3;overflow-wrap:anywhere}
81
+ .graview-bar-item:hover{background:color-mix(in srgb,var(--graview-ink) 7%,transparent)}
82
+ .graview-bar-item[aria-current]{font-weight:600;background:color-mix(in srgb,var(--graview-accent) 14%,transparent)}
83
+ .graview-bar-mark{display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto;width:14px;height:1.3em;color:var(--graview-ink-muted)}
84
+ .graview-bar :focus-visible{outline:2px solid var(--graview-accent);outline-offset:1px}
85
+ .graview-bar-tools{display:flex;align-items:center;flex:0 0 auto;gap:8px;margin-left:auto}
86
+ .graview-bar-find{display:flex;width:15rem;min-width:7rem;flex:0 1 auto}
87
+ .graview-bar-find-open{display:none}
88
+ .graview-bar-line{display:flex;flex:0 0 auto;padding:6px 8px;background:var(--graview-ground);border-bottom:1px solid var(--graview-edge);font-family:var(--graview-font-body,system-ui)}
89
+ .graview-bar-line .graview-bar-place{height:auto;min-height:32px}
90
+ .graview-bar-line .graview-bar-place-words{white-space:normal}
76
91
  @container (max-width: 639px){
77
- .graview-bar-row{grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"app tools" "places places";padding:0 12px;min-height:0;column-gap:12px}
78
- .graview-bar-app,.graview-bar-tools{min-height:48px}
79
- .graview-bar-places{border-top:1px solid var(--graview-edge);margin:0 -12px;padding:0 2px;min-height:40px}
80
- .graview-bar-find{display:none;width:auto;flex:1 1 auto}
92
+ .graview-bar-row{gap:10px;padding:0 12px}
93
+ .graview-bar-app{max-width:none;flex:1 1 auto}
94
+ .graview-bar-app-name{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow:hidden}
95
+ .graview-bar-face{padding:0 8px}
96
+ .graview-bar-face span{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
97
+ .graview-bar-mid,.graview-bar-find{display:none}
81
98
  .graview-bar-find-open{display:inline-flex}
82
- .graview-bar[data-finding] .graview-bar-app{display:none}
83
- .graview-bar[data-finding] .graview-bar-row{grid-template-areas:"tools tools" "places places"}
99
+ .graview-bar[data-finding] :is(.graview-bar-app,.graview-bar-faces,.graview-bar-find-open){display:none}
84
100
  .graview-bar[data-finding] .graview-bar-tools{flex:1 1 auto}
85
- .graview-bar[data-finding] .graview-bar-find{display:flex}
86
- .graview-bar[data-finding] .graview-bar-find-open{display:none}
101
+ .graview-bar[data-finding] .graview-bar-find{display:flex;flex:1 1 auto;width:auto;min-width:0}
87
102
  }`;
88
103
  /** A tool's own box: one size, square, its name in words for whoever cannot see the mark. */
89
104
  export const toolStyle = {
@@ -106,11 +121,13 @@ export const toolStyle = {
106
121
  };
107
122
  /**
108
123
  * THE BAR. The app at the left — its mark, its name as the page's heading,
109
- * the way home — its places in the middle, and the tools at the right: the
110
- * Find a face puts in it, then `tools` (the standing and the person). One
111
- * row at a desk; on a phone the places take a second row of their own.
124
+ * the way home — then the switch between the scene and the pages (FR-137),
125
+ * then, on Pages, the place you are on (FR-138), and the tools at the
126
+ * right: the Find a face puts in it, then `tools` (the standing and the
127
+ * person). One row of one height on a desk and a phone; on a phone the
128
+ * place is the page's first line, under the bar.
112
129
  */
113
- export function AppBar({ brand, name, heading = 1, home, places, current, reach, tools, find = true, description, findBox, onFind, }) {
130
+ export function AppBar({ brand, name, heading = 1, home, faces, places, current, reach, tools, find = true, description, findBox, onFind, }) {
114
131
  const bar = useRef(null);
115
132
  const [slot, setSlot] = useState(null);
116
133
  const [compact, setCompact] = useState(false);
@@ -151,31 +168,59 @@ export function AppBar({ brand, name, heading = 1, home, places, current, reach,
151
168
  requestAnimationFrame(() => opener.current?.focus());
152
169
  }, []);
153
170
  const Heading = heading === false ? "span" : `h${heading}`;
171
+ const Home = (home.href !== undefined ? "a" : "button");
154
172
  const press = (go) => (event) => {
155
173
  if (!go || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
156
174
  return;
157
175
  event.preventDefault();
158
176
  go();
159
177
  };
160
- return (_jsxs("header", { ref: bar, className: "graview-bar", "data-testid": "app-bar", "data-graview-app-bar": "", ...(finding && compact ? { "data-finding": "" } : {}), children: [_jsx("style", { children: BAR_CSS }), _jsxs("div", { className: "graview-bar-row", children: [_jsx("div", { className: "graview-bar-app", children: _jsx(Heading, { className: "graview-bar-name", children: home.href !== undefined ? (_jsxs("a", { href: home.href, onClick: press(home.go), "data-testid": "app-home", ...(home.current ? { "aria-current": "page" } : {}), title: description ? `${name} — ${description}` : `${name} — home`, className: "graview-bar-home", children: [_jsx(AppMark, { brand: brand, size: 20 }), _jsx("span", { "data-testid": "app-name", children: name })] })) : (_jsxs("button", { type: "button", onClick: home.go, "data-testid": "app-home", ...(home.current ? { "aria-current": "page" } : {}), title: description ? `${name} — ${description}` : `${name} — home`, className: "graview-bar-home", children: [_jsx(AppMark, { brand: brand, size: 20 }), _jsx("span", { "data-testid": "app-name", children: name })] })) }) }), _jsx(BarPlaces, { places: places, current: current, reach: reach }), _jsxs("div", { className: "graview-bar-tools", children: [find ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "graview-bar-find-open", ref: opener, "data-testid": "app-find-open", "aria-label": "Find", title: "Find", onClick: openFind, style: { ...toolStyle, display: undefined }, children: _jsx(FindMark, {}) }), _jsx("div", { ref: setSlot, className: "graview-bar-find", "data-testid": "app-find", onBlur: (event) => {
161
- // Put away on a phone when the keyboard leaves it with nothing typed.
162
- const next = event.relatedTarget;
163
- if (next instanceof Node && event.currentTarget.contains(next))
164
- return;
165
- if (!event.currentTarget.querySelector("input")?.value)
166
- putAway();
167
- }, onKeyDownCapture: (event) => {
168
- if (event.key === "Escape" && compact)
169
- escaping.current = true;
170
- }, onKeyDown: (event) => {
171
- if (event.key !== "Escape" || !compact)
172
- return;
173
- // The scene's Find takes Escape for its words first; an empty box is put away.
174
- if (!event.defaultPrevented || !event.currentTarget.querySelector("input")?.value)
175
- putAway();
176
- else
177
- escaping.current = false;
178
- }, children: findBox })] })) : null, tools] })] })] }));
178
+ // The place control stands on Pages: in the bar on a desk, as the page's first line on a phone.
179
+ const onPages = !faces || faces.pages.current;
180
+ const placeControl = onPages && places.length > 0 ? _jsx(PlaceControl, { places: places, current: current, reach: reach }) : null;
181
+ return (_jsxs(_Fragment, { children: [_jsxs("header", { ref: bar, className: "graview-bar", "data-testid": "app-bar", "data-graview-app-bar": "", ...(finding && compact ? { "data-finding": "" } : {}), children: [_jsx("style", { children: BAR_CSS }), _jsxs("div", { className: "graview-bar-row", children: [_jsx("div", { className: "graview-bar-app", children: _jsx(Heading, { className: "graview-bar-name", children: _jsxs(Home, { ...(home.href !== undefined ? { href: home.href, onClick: press(home.go) } : { type: "button", onClick: home.go }), "data-testid": "app-home", ...(home.current ? { "aria-current": "page" } : {}), title: description ? `${name} — ${description}` : `${name} — home`, className: "graview-bar-home", children: [_jsx(AppMark, { brand: brand, size: 20 }), _jsx("span", { className: "graview-bar-app-name", "data-testid": "app-name", children: name })] }) }) }), faces ? _jsx(FaceSwitch, { faces: faces }) : null, _jsx("div", { className: "graview-bar-mid", children: compact ? null : placeControl }), _jsxs("div", { className: "graview-bar-tools", children: [find ? (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "graview-bar-find-open", ref: opener, "data-testid": "app-find-open", "aria-label": "Find", title: "Find", onClick: openFind, style: { ...toolStyle, display: undefined }, children: _jsx(FindMark, {}) }), _jsx("div", { ref: setSlot, className: "graview-bar-find", "data-testid": "app-find", onBlur: (event) => {
182
+ // Put away on a phone when the keyboard leaves it with nothing typed.
183
+ const next = event.relatedTarget;
184
+ if (next instanceof Node && event.currentTarget.contains(next))
185
+ return;
186
+ if (!event.currentTarget.querySelector("input")?.value)
187
+ putAway();
188
+ }, onKeyDownCapture: (event) => {
189
+ if (event.key === "Escape" && compact)
190
+ escaping.current = true;
191
+ }, onKeyDown: (event) => {
192
+ if (event.key !== "Escape" || !compact)
193
+ return;
194
+ // The scene's Find takes Escape for its words first; an empty box is put away.
195
+ if (!event.defaultPrevented || !event.currentTarget.querySelector("input")?.value)
196
+ putAway();
197
+ else
198
+ escaping.current = false;
199
+ }, children: findBox })] })) : null, tools] })] })] }), compact && placeControl ? (_jsx("div", { className: "graview-bar-line", "data-graview-place-line": "", "data-testid": "app-place-line", children: placeControl })) : null] }));
200
+ }
201
+ /**
202
+ * THE SWITCH (FR-137): the scene and the pages, side by side, an icon and a
203
+ * word each — the words the accessible names alone on a phone — the one
204
+ * drawn pressed. Choosing the scene draws it under the bar; choosing the
205
+ * pages goes back to the page you were on.
206
+ */
207
+ function FaceSwitch({ faces }) {
208
+ const face = (which) => {
209
+ const one = faces[which];
210
+ return (_jsxs("button", { type: "button", className: "graview-bar-face", "data-testid": `app-face-${which}`, "aria-pressed": one.current, title: one.label, onClick: () => {
211
+ if (!one.current)
212
+ one.go();
213
+ }, children: [which === "scene" ? _jsx(SceneMark, {}) : _jsx(PagesMark, {}), _jsx("span", { children: one.label })] }));
214
+ };
215
+ return (_jsxs("div", { className: "graview-bar-faces", role: "group", "aria-label": `${faces.scene.label} or ${faces.pages.label}`, "data-testid": "app-faces", children: [face("scene"), face("pages")] }));
216
+ }
217
+ /** The scene's mark: a plot of the city, seen from above at an angle. */
218
+ function SceneMark() {
219
+ return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", focusable: "false", style: { flex: "0 0 auto" }, children: [_jsx("path", { d: "M7 1.5 L12.5 4.5 L7 7.5 L1.5 4.5 Z", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }), _jsx("path", { d: "M1.5 7.5 L7 10.5 L12.5 7.5", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round", strokeLinecap: "round" })] }));
220
+ }
221
+ /** The pages' mark: a page of lines. */
222
+ function PagesMark() {
223
+ return (_jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", focusable: "false", style: { flex: "0 0 auto" }, children: [_jsx("rect", { x: "2.25", y: "1.75", width: "9.5", height: "10.5", rx: "1.5", fill: "none", stroke: "currentColor", strokeWidth: "1.3" }), _jsx("path", { d: "M4.5 5 H9.5 M4.5 7.25 H9.5 M4.5 9.5 H7.5", stroke: "currentColor", strokeWidth: "1.2", strokeLinecap: "round" })] }));
179
224
  }
180
225
  /** The way a menu opens: a chevron drawn, not a glyph a face may draw as a dot. */
181
226
  function Chevron() {
@@ -185,67 +230,43 @@ function Chevron() {
185
230
  function FindMark() {
186
231
  return (_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", focusable: "false", children: [_jsx("circle", { cx: "7", cy: "7", r: "4.75", fill: "none", stroke: "currentColor", strokeWidth: "1.5" }), _jsx("path", { d: "M10.5 10.5 L14 14", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" })] }));
187
232
  }
233
+ /** A place's mark in the list: the home's house, a kind's plot in its own hue. */
234
+ function PlaceMark({ place }) {
235
+ if (place.group === "home") {
236
+ return (_jsx("svg", { width: "13", height: "13", viewBox: "0 0 14 14", "aria-hidden": "true", focusable: "false", children: _jsx("path", { d: "M2 6.5 L7 2 L12 6.5 V12 H2 Z", fill: "none", stroke: "currentColor", strokeWidth: "1.3", strokeLinejoin: "round" }) }));
237
+ }
238
+ // A kind's plot, as the scene draws it from altitude (the mark FR-117 gave a kind with no drawing); a picture of it wears its outline; how the kinds connect, the ink's.
239
+ const hue = place.kind ? `hsl(${Math.round(hueFor(place.kind))} 55% 52%` : "rgb(128 128 128";
240
+ return (_jsx("span", { "aria-hidden": "true", style: {
241
+ display: "inline-block",
242
+ width: 12,
243
+ height: 8,
244
+ clipPath: "polygon(50% 0, 100% 50%, 50% 100%, 0 50%)",
245
+ background: place.group === "lists" ? `${hue})` : `linear-gradient(${hue}), ${hue})) center / 6px 4px no-repeat, ${hue} / 0.35)`,
246
+ } }));
247
+ }
248
+ const GROUPS = [{ group: "home" }, { group: "lists", heading: "Lists" }, { group: "pictures", heading: "Pictures" }];
188
249
  /**
189
- * THE PLACES AS PLAIN TABS. As many as the row holds, in order; the rest
190
- * under "More". The tab you are on is marked, and always on the row — in
191
- * the last tab's room when the row could not otherwise hold it, and alone
192
- * beside "More" when the row holds none — so where you are is never hidden.
250
+ * THE PLACE YOU ARE ON, AND EVERY OTHER (FR-138): one control, the place's
251
+ * name and a chevron, that opens the app's places — the home first, then
252
+ * the Lists and the Pictures, each with its mark, a long name wrapped
253
+ * rather than cut. Every place is two presses away, and the bar is one row
254
+ * of one height however many there are.
193
255
  */
194
- function BarPlaces({ places, current, reach }) {
195
- const row = useRef(null);
196
- const measure = useRef(null);
197
- const [fits, setFits] = useState(places.length);
256
+ function PlaceControl({ places, current, reach }) {
198
257
  const popover = usePopover("places");
199
- const signature = places.map((place) => place.label).join("|");
200
- const currentAt = places.findIndex((place) => place.key === current);
201
- useLayoutEffect(() => {
202
- const element = row.current;
203
- const ruler = measure.current;
204
- if (!element || !ruler)
205
- return;
206
- const fit = () => {
207
- const room = element.getBoundingClientRect().width + 0.5;
208
- const widths = [...ruler.children].map((child) => child.getBoundingClientRect().width);
209
- const more = widths.pop() ?? 0;
210
- const sum = (list) => list.reduce((total, width) => total + width, 0);
211
- let count = widths.length;
212
- if (sum(widths) > room) {
213
- // As many as fit beside "More" — the place you are on among them, in the last one's room when it is further along.
214
- count = widths.length - 1;
215
- while (count > 0) {
216
- const seen = currentAt >= count ? [...widths.slice(0, count - 1), widths[currentAt]] : widths.slice(0, count);
217
- if (sum(seen) + more <= room)
218
- break;
219
- count -= 1;
220
- }
221
- }
222
- setFits((was) => (was === count ? was : count));
223
- };
224
- fit();
225
- if (typeof ResizeObserver === "undefined")
226
- return;
227
- const watch = new ResizeObserver(fit);
228
- watch.observe(element);
229
- // The ruler too: a brand's web font arriving after the first measure widens every tab.
230
- watch.observe(ruler);
231
- return () => watch.disconnect();
232
- }, [signature, currentAt]);
233
- if (places.length === 0)
234
- return _jsx("div", { className: "graview-bar-places", "data-testid": "app-places", ref: row });
235
- // The place you are on is on the row: in the last tab's room when it is one the row could not otherwise hold, alone when it holds none.
236
- const shown = currentAt >= fits ? [...places.slice(0, Math.max(0, fits - 1)), places[currentAt]] : places.slice(0, fits);
237
- const rest = places.filter((place) => !shown.includes(place));
238
- const tab = (place, inMenu) => {
239
- const here = place.key === current;
258
+ const here = places.find((place) => place.key === current);
259
+ const said = here?.label ?? "Places";
260
+ const entry = (place) => {
261
+ const at = place.key === current;
240
262
  const href = reach.href?.(place.path);
241
263
  const common = {
242
- className: inMenu ? "graview-bar-item" : "graview-bar-tab",
264
+ className: "graview-bar-item",
243
265
  "data-testid": `app-place-${place.key}`,
244
266
  "data-place-path": place.path,
245
- ...(here ? { "aria-current": "page" } : {}),
267
+ ...(at ? { "aria-current": "page" } : {}),
246
268
  onClick: (event) => {
247
- if (inMenu)
248
- popover.setOpen(false);
269
+ popover.setOpen(false);
249
270
  if (!reach.go)
250
271
  return;
251
272
  if (href !== undefined && (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey))
@@ -254,8 +275,15 @@ function BarPlaces({ places, current, reach }) {
254
275
  reach.go(place);
255
276
  },
256
277
  };
257
- return href !== undefined ? (_jsx("a", { ...common, href: href, children: place.label }, place.key)) : (_jsx("button", { ...common, type: "button", children: place.label }, place.key));
278
+ const Entry = (href !== undefined ? "a" : "button");
279
+ return (_jsxs(Entry, { ...common, ...(href !== undefined ? { href } : { type: "button" }), children: [_jsx("span", { className: "graview-bar-mark", children: _jsx(PlaceMark, { place: place }) }), _jsx("span", { children: place.label })] }, place.key));
258
280
  };
259
- return (_jsxs("nav", { ref: row, className: "graview-bar-places", "aria-label": "The app\u2019s places", "data-testid": "app-places", style: { position: "relative" }, children: [_jsxs("div", { ref: measure, className: "graview-bar-measure", "aria-hidden": "true", children: [places.map((place) => (_jsx("span", { className: "graview-bar-tab", style: { fontWeight: 600 }, children: place.label }, place.key))), _jsxs("span", { className: "graview-bar-tab", children: ["More", _jsx(Chevron, {})] })] }), shown.map((place) => tab(place, false)), rest.length > 0 ? (_jsxs("span", { style: { position: "relative", display: "inline-flex" }, children: [_jsxs("button", { type: "button", className: "graview-bar-tab", "data-testid": "app-places-more", ...popover.trigger, onClick: popover.toggle, "aria-label": "More places", children: ["More", _jsx(Chevron, {})] }), _jsx("ul", { ...popover.pane, "data-testid": "app-places-more-list", "aria-label": "More places", hidden: !popover.open, className: "graview-bar-more", style: POPOVER_STYLE, children: rest.map((place) => (_jsx("li", { children: tab(place, true) }, place.key))) })] })) : null] }));
281
+ return (_jsxs("span", { className: "graview-bar-place-at", children: [_jsxs("button", { type: "button", className: "graview-bar-place", "data-testid": "app-places-open", ...popover.trigger, onClick: popover.toggle, "aria-label": `${said} — every place`, title: said, children: [_jsx("span", { className: "graview-bar-place-words", "data-testid": "app-place-current", children: said }), _jsx(Chevron, {})] }), _jsx("nav", { ...popover.pane, "data-testid": "app-places", "aria-label": "The app\u2019s places", hidden: !popover.open, className: "graview-bar-list", style: POPOVER_STYLE, children: GROUPS.map(({ group, heading }) => {
282
+ const listed = places.filter((place) => place.group === group);
283
+ if (listed.length === 0)
284
+ return null;
285
+ const id = `${popover.pane.id}-${group}`;
286
+ return (_jsxs("div", { role: "group", ...(heading ? { "aria-labelledby": id } : { "aria-label": "Home" }), "data-place-group": group, children: [heading ? (_jsx("p", { id: id, children: heading })) : null, _jsx("ul", { children: listed.map((place) => (_jsx("li", { children: entry(place) }, place.key))) })] }, group));
287
+ }) })] }));
260
288
  }
261
289
  //# sourceMappingURL=app-bar.js.map