create-pmos 0.83.0 → 0.85.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/payload/planning/evals/eval-calibration.md +1 -1
- package/payload/planning/okrs/health-budgets.md +1 -1
- package/payload/state/discoveries.md +1 -1
- package/workspace/guide/command-palette.md +4 -2
- package/workspace/guide/layout.md +4 -4
- package/workspace/package-lock.json +2 -2
- package/workspace/package.json +1 -1
- package/workspace/public/pm/components.css +232 -11
- package/workspace/public/pm/dest-settings.js +59 -43
- package/workspace/public/pm/palette-copy.js +4 -0
- package/workspace/public/pm/palette.js +39 -8
- package/workspace/public/pm/primitives.js +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-pmos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"description": "Scaffold the PMOS portable kit — run the PMOS product-management workflow (initiative → lane-fit spec → contract → build → three gates, with work-type lanes for features, research, maintenance, incidents, and content) entirely inside your own repo, with all state as git-versioned files. Includes `workflow-sweep`, which audits the state every board derives from and separates real defects from work you simply have not done. `create-pmos workspace` also installs + launches the local Terminal Workspace GUI. No database, no network, no telemetry.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
package/payload/KIT-VERSION
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Eval calibration log"
|
|
4
4
|
description: "Append-only record of every PM override of an evaluator verdict."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T16:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Health budgets"
|
|
4
4
|
description: "Closed registry of health-budget anchors (D59), edited only by the PM."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T16:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
type: Reference
|
|
3
3
|
title: "Discoveries and friction log"
|
|
4
4
|
description: "Append-only record of what runs discovered and where the process rubbed."
|
|
5
|
-
generated: {by: process:create-pmos, at: "2026-10-07T16:
|
|
5
|
+
generated: {by: process:create-pmos, at: "2026-10-07T16:18:36Z"}
|
|
6
6
|
status: stable
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
type: Guide
|
|
3
3
|
title: "Command palette"
|
|
4
4
|
description: "How to use the ⌘K palette: run a command, see its command-line form, keep a note, ask the bound session, search Knowledge, Memory and the Guide, and use the compose line on a phone."
|
|
5
|
-
timestamp: 2026-10-
|
|
5
|
+
timestamp: 2026-10-07
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Command palette
|
|
9
9
|
|
|
10
10
|
The command palette is one box for three jobs: open something, run a command, or keep a note. Press **⌘K** (Ctrl+K on Windows and Linux) to open it. Press **Esc** to close it.
|
|
11
11
|
|
|
12
|
+
On a desktop, the palette is a dialog in the middle of the screen. The search box is at the top with a search icon, the scope switch is under it, and a footer at the bottom shows a hint and the keys. On a phone it is a sheet (see "On a phone").
|
|
13
|
+
|
|
12
14
|
## Rows
|
|
13
15
|
|
|
14
|
-
Type in the box. The rows change as you type.
|
|
16
|
+
Type in the box. The rows change as you type. Rows of one kind sit under one small label, for example Commands or Initiatives, and each row has an icon for its kind. The selected row has a ring and an arrow. The rows come in this order:
|
|
15
17
|
|
|
16
18
|
1. **Matched rows.** A command, an initiative, a repository, a session, a tab or a view whose name matches what you typed. A command shows its command-line form next to its name, for example `pmos item`. You can run the same command from a terminal.
|
|
17
19
|
2. **Capture as note.** Keeps what you typed as a note.
|
|
@@ -27,16 +27,16 @@ Each place is drawn when its run is built. Until then the place says which run b
|
|
|
27
27
|
|
|
28
28
|
## Settings
|
|
29
29
|
|
|
30
|
-
Settings holds settings only, in eight sections:
|
|
30
|
+
Settings holds settings only, in eight sections. On a wide screen they are cards in two columns, like Agenda: Repositories, New sessions, Profiles and Theme on the left, and Validate consent, People, Online and Updates on the right. On a narrow screen the cards stack in one column. Each switch (Validate consent, People, Online) turns on when you press it. **Check for updates** is at the top right of the page.
|
|
31
31
|
|
|
32
32
|
- **Repositories**: every registered repository. Press a repository to make it the current one. Press its ⋯ for **Rename** or **Remove from PMOS**. Each asks in a sheet, and a remove asks you to confirm. **Add repository** shows the scan of a folder before you add it.
|
|
33
33
|
- **Profiles**: the Claude profiles a session can start with. They open in the detail.
|
|
34
34
|
- **New sessions**: the default CLI and the default model for a new session in the dock.
|
|
35
|
-
- **Validate consent**: the consent of the current repository.
|
|
35
|
+
- **Validate consent**: the consent of the current repository. Use its switch to turn it on or off. **Open Validate** opens the Validate page as a tab.
|
|
36
36
|
- **People**: whether the morning brief keeps attendee names from the calendar for the current repository. It is off until you turn it on. Turning it off also hides the names recorded before.
|
|
37
|
-
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then
|
|
37
|
+
- **Online**: puts this workspace at your own address, such as `pmos.pm-os.app`, through a Cloudflare tunnel. Enter the public address, the tunnel name, your Cloudflare team domain, the Access audience (AUD) tag and your email, then turn on the **Online** switch. The workspace runs the tunnel and remembers the switch when it restarts. On your phone, open the address and sign in with Cloudflare Access (Google or an email code). The workspace does not ask for its token there. Only the computer that runs the workspace can change this section. A phone sees whether it is on. When a sign-in at the address is refused, this section names the check that failed, such as the email or the AUD tag. [REMOTE.md](../REMOTE.md) has the one-time Cloudflare steps.
|
|
38
38
|
- **Theme**: Dark or Light.
|
|
39
|
-
- **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, one button shows: **Update to** and the version. It updates every part that is older, in this order:
|
|
39
|
+
- **Updates**: the version of the workspace app and the version of the kit in the current repository. Press **Check for updates** at the top of the page to compare them with the latest published version of PMOS. This is the only time the workspace asks the internet for a version, and it asks only when you press. When a newer version is available, one button shows: **Update to** and the version. It updates every part that is older, in this order:
|
|
40
40
|
- **The kit** in the current repository, when it is older. This does what `npx create-pmos@latest --update` does in a terminal. It changes only the kit's own files under `pmos/` (skills, scripts, templates and guides). Your records in `state/`, `planning/` and `okf/product/` do not change. The last lines of the output show under the button. The changed files are under `pmos/`, which Sync treats as records, so Sync (or automatic sync) sends them with your other records. To read them first, look at `git diff pmos/` before you sync.
|
|
41
41
|
- **The workspace app**, when it is older. This does what `pmos upgrade` does in a terminal. The button asks you first. The workspace stops, installs the new version and starts again, and the page reloads when the new version starts. The reloaded page runs the new version's code: each start gives the page files a new address, so neither the browser nor the Online address's tunnel can keep the old ones. You do not need a terminal or a hard refresh. Sessions keep running in tmux. It works on the computer that runs the workspace and at its Online address, because the page comes back there after the restart. It also works on a phone or public address that signs in with a token set in `PMOS_TOKEN`, which the workspace keeps. When the workspace made its own token at start, the token changes on the restart, so the page refuses and says where to press it. When the workspace runs from a PMOS checkout, update it with `git pull` instead.
|
|
42
42
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pmos-workspace",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "pmos-workspace",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.85.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@xterm/addon-fit": "^0.11.0",
|
package/workspace/package.json
CHANGED
|
@@ -696,6 +696,14 @@
|
|
|
696
696
|
height: auto;
|
|
697
697
|
max-height: calc(var(--pmc-vvh, 100dvh) - 24px);
|
|
698
698
|
}
|
|
699
|
+
.pmc-sheet--center {
|
|
700
|
+
inset: auto;
|
|
701
|
+
top: 84px;
|
|
702
|
+
left: 50%;
|
|
703
|
+
width: 640px;
|
|
704
|
+
max-height: 70vh;
|
|
705
|
+
transform: translateX(-50%);
|
|
706
|
+
}
|
|
699
707
|
.pmc-sheet__handle {
|
|
700
708
|
flex: none;
|
|
701
709
|
width: 36px;
|
|
@@ -723,11 +731,33 @@
|
|
|
723
731
|
letter-spacing: -0.01em;
|
|
724
732
|
overflow-wrap: anywhere;
|
|
725
733
|
}
|
|
734
|
+
.pmc-sheet__title--hidden {
|
|
735
|
+
position: absolute;
|
|
736
|
+
width: 1px;
|
|
737
|
+
height: 1px;
|
|
738
|
+
padding: 0;
|
|
739
|
+
margin: -1px;
|
|
740
|
+
overflow: hidden;
|
|
741
|
+
clip: rect(0, 0, 0, 0);
|
|
742
|
+
white-space: nowrap;
|
|
743
|
+
border-width: 0;
|
|
744
|
+
}
|
|
726
745
|
.pmc-sheet__sub {
|
|
727
746
|
font-size: 12px;
|
|
728
747
|
line-height: 16px;
|
|
729
748
|
color: var(--muted-foreground);
|
|
730
749
|
}
|
|
750
|
+
.pmc-sheet__sub--hidden {
|
|
751
|
+
position: absolute;
|
|
752
|
+
width: 1px;
|
|
753
|
+
height: 1px;
|
|
754
|
+
padding: 0;
|
|
755
|
+
margin: -1px;
|
|
756
|
+
overflow: hidden;
|
|
757
|
+
clip: rect(0, 0, 0, 0);
|
|
758
|
+
white-space: nowrap;
|
|
759
|
+
border-width: 0;
|
|
760
|
+
}
|
|
731
761
|
.pmc-sheet__head .pmc-btn--icon {
|
|
732
762
|
margin: -4px -8px 0 0;
|
|
733
763
|
}
|
|
@@ -741,6 +771,16 @@
|
|
|
741
771
|
overscroll-behavior: contain;
|
|
742
772
|
padding: 16px;
|
|
743
773
|
}
|
|
774
|
+
.pmc-sheet--center .pmc-sheet__body {
|
|
775
|
+
gap: 0;
|
|
776
|
+
padding: 0;
|
|
777
|
+
overflow: hidden;
|
|
778
|
+
min-height: 0;
|
|
779
|
+
}
|
|
780
|
+
.pmc-sheet--center {
|
|
781
|
+
display: flex;
|
|
782
|
+
flex-direction: column;
|
|
783
|
+
}
|
|
744
784
|
.pmc-sheet__body > * {
|
|
745
785
|
flex: none;
|
|
746
786
|
}
|
|
@@ -2359,18 +2399,22 @@
|
|
|
2359
2399
|
.fr-body[data-fr-dock="rail"] > .fr-dock {
|
|
2360
2400
|
display: none !important;
|
|
2361
2401
|
}
|
|
2362
|
-
/* Settings:
|
|
2402
|
+
/* Settings: a full-width two-column grid of cards, laid out like Agenda. The grid sits in a container so it folds to one
|
|
2403
|
+
* column by the width of the page, not the window. */
|
|
2363
2404
|
.fr-set {
|
|
2364
2405
|
display: flex;
|
|
2365
2406
|
flex-direction: column;
|
|
2366
|
-
gap:
|
|
2367
|
-
|
|
2368
|
-
padding:
|
|
2407
|
+
gap: 12px;
|
|
2408
|
+
width: 100%;
|
|
2409
|
+
padding: 16px 16px 48px;
|
|
2410
|
+
container-type: inline-size;
|
|
2369
2411
|
}
|
|
2370
2412
|
.fr-set__intro {
|
|
2371
2413
|
display: flex;
|
|
2372
|
-
|
|
2373
|
-
|
|
2414
|
+
align-items: center;
|
|
2415
|
+
justify-content: space-between;
|
|
2416
|
+
gap: 12px;
|
|
2417
|
+
min-height: 32px;
|
|
2374
2418
|
}
|
|
2375
2419
|
.fr-set__h1 {
|
|
2376
2420
|
margin: 0;
|
|
@@ -2382,10 +2426,39 @@
|
|
|
2382
2426
|
color: var(--muted-foreground);
|
|
2383
2427
|
font-size: 12px;
|
|
2384
2428
|
}
|
|
2429
|
+
.fr-set__grid {
|
|
2430
|
+
display: grid;
|
|
2431
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
2432
|
+
grid-template-rows: repeat(5, auto);
|
|
2433
|
+
gap: 12px;
|
|
2434
|
+
align-items: stretch;
|
|
2435
|
+
}
|
|
2436
|
+
/* Left: repositories, new sessions, profiles, theme. Right: Validate, People, Online, updates. The DOM order stays
|
|
2437
|
+
* repositories, profiles, sessions, validate, people, online, theme, updates; Online spans two rows so the columns stay level. */
|
|
2438
|
+
.fr-set__section[data-fr-section="repositories"] { grid-column: 1; grid-row: 1 / span 1; }
|
|
2439
|
+
.fr-set__section[data-fr-section="sessions"] { grid-column: 1; grid-row: 2 / span 1; }
|
|
2440
|
+
.fr-set__section[data-fr-section="profiles"] { grid-column: 1; grid-row: 3 / span 1; }
|
|
2441
|
+
.fr-set__section[data-fr-section="theme"] { grid-column: 1; grid-row: 4 / span 1; }
|
|
2442
|
+
.fr-set__section[data-fr-section="validate"] { grid-column: 2; grid-row: 1 / span 1; }
|
|
2443
|
+
.fr-set__section[data-fr-section="people"] { grid-column: 2; grid-row: 2 / span 1; }
|
|
2444
|
+
.fr-set__section[data-fr-section="online"] { grid-column: 2; grid-row: 3 / span 2; }
|
|
2445
|
+
.fr-set__section[data-fr-section="updates"] { grid-column: 2; grid-row: 5 / span 1; }
|
|
2446
|
+
@container (max-width: 700px) {
|
|
2447
|
+
.fr-set__grid {
|
|
2448
|
+
grid-template-columns: minmax(0, 1fr);
|
|
2449
|
+
grid-template-rows: none;
|
|
2450
|
+
}
|
|
2451
|
+
.fr-set__section[data-fr-section] { grid-column: 1; grid-row: auto; }
|
|
2452
|
+
}
|
|
2385
2453
|
.fr-set__section {
|
|
2386
2454
|
display: flex;
|
|
2387
2455
|
flex-direction: column;
|
|
2388
2456
|
gap: 8px;
|
|
2457
|
+
min-width: 0;
|
|
2458
|
+
padding: 10px 12px;
|
|
2459
|
+
border: 1px solid var(--border);
|
|
2460
|
+
border-radius: 8px;
|
|
2461
|
+
background: var(--card);
|
|
2389
2462
|
}
|
|
2390
2463
|
.fr-set__head {
|
|
2391
2464
|
display: flex;
|
|
@@ -2395,14 +2468,27 @@
|
|
|
2395
2468
|
min-height: 32px;
|
|
2396
2469
|
}
|
|
2397
2470
|
.fr-set__title {
|
|
2471
|
+
display: flex;
|
|
2472
|
+
align-items: center;
|
|
2473
|
+
gap: 6px;
|
|
2398
2474
|
margin: 0;
|
|
2399
|
-
|
|
2400
|
-
font-
|
|
2475
|
+
color: var(--muted-foreground);
|
|
2476
|
+
font-size: 11px;
|
|
2477
|
+
font-weight: 500;
|
|
2478
|
+
letter-spacing: 0.04em;
|
|
2479
|
+
text-transform: uppercase;
|
|
2401
2480
|
}
|
|
2402
2481
|
.fr-set__fields {
|
|
2403
2482
|
display: flex;
|
|
2404
2483
|
flex-wrap: wrap;
|
|
2405
|
-
gap:
|
|
2484
|
+
gap: 10px;
|
|
2485
|
+
}
|
|
2486
|
+
.fr-set__fields--grid {
|
|
2487
|
+
display: grid;
|
|
2488
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
2489
|
+
}
|
|
2490
|
+
.fr-set__fields--grid > * {
|
|
2491
|
+
min-width: 0;
|
|
2406
2492
|
}
|
|
2407
2493
|
.fr-set__line {
|
|
2408
2494
|
display: flex;
|
|
@@ -2410,6 +2496,32 @@
|
|
|
2410
2496
|
justify-content: space-between;
|
|
2411
2497
|
gap: 12px;
|
|
2412
2498
|
}
|
|
2499
|
+
.fr-set__line--box {
|
|
2500
|
+
justify-content: flex-start;
|
|
2501
|
+
gap: 10px;
|
|
2502
|
+
padding: 8px 10px;
|
|
2503
|
+
border: 1px solid var(--border);
|
|
2504
|
+
border-radius: 8px;
|
|
2505
|
+
background: var(--background);
|
|
2506
|
+
font-size: 14px;
|
|
2507
|
+
}
|
|
2508
|
+
.fr-set__sw {
|
|
2509
|
+
display: flex;
|
|
2510
|
+
align-items: center;
|
|
2511
|
+
justify-content: space-between;
|
|
2512
|
+
gap: 12px;
|
|
2513
|
+
padding: 8px 10px;
|
|
2514
|
+
border: 1px solid var(--border);
|
|
2515
|
+
border-radius: 8px;
|
|
2516
|
+
background: var(--background);
|
|
2517
|
+
}
|
|
2518
|
+
.fr-set__sw-text {
|
|
2519
|
+
flex: 1;
|
|
2520
|
+
min-width: 0;
|
|
2521
|
+
display: grid;
|
|
2522
|
+
gap: 2px;
|
|
2523
|
+
font-size: 14px;
|
|
2524
|
+
}
|
|
2413
2525
|
.fr-set__add {
|
|
2414
2526
|
padding: 12px 0;
|
|
2415
2527
|
}
|
|
@@ -2422,6 +2534,7 @@
|
|
|
2422
2534
|
overflow: auto;
|
|
2423
2535
|
padding: 8px;
|
|
2424
2536
|
border: 1px solid var(--border);
|
|
2537
|
+
border-radius: 6px;
|
|
2425
2538
|
background: var(--muted);
|
|
2426
2539
|
color: var(--foreground);
|
|
2427
2540
|
font-family: var(--mono);
|
|
@@ -2441,9 +2554,13 @@
|
|
|
2441
2554
|
}
|
|
2442
2555
|
}
|
|
2443
2556
|
@media (pointer: coarse) {
|
|
2444
|
-
.fr-set__head
|
|
2557
|
+
.fr-set__head,
|
|
2558
|
+
.fr-set__intro {
|
|
2445
2559
|
min-height: 44px;
|
|
2446
2560
|
}
|
|
2561
|
+
.fr-set__sw {
|
|
2562
|
+
min-height: 48px;
|
|
2563
|
+
}
|
|
2447
2564
|
/* The dock's own strip and bottom bar (panel-tabs.js) size their controls inline at 22-30px; under touch each is a
|
|
2448
2565
|
* 44px target. The strip's fixed height yields to them. */
|
|
2449
2566
|
.fr-dock [data-tabs="bar"] {
|
|
@@ -3595,13 +3712,49 @@
|
|
|
3595
3712
|
overscroll-behavior: contain;
|
|
3596
3713
|
}
|
|
3597
3714
|
.pl-item[aria-selected="true"] {
|
|
3598
|
-
background: var(--accent);
|
|
3715
|
+
background: var(--accent) !important;
|
|
3716
|
+
box-shadow: 0 0 0 1px var(--ring);
|
|
3717
|
+
}
|
|
3718
|
+
.pl-icon-tile {
|
|
3719
|
+
display: flex;
|
|
3720
|
+
align-items: center;
|
|
3721
|
+
justify-content: center;
|
|
3722
|
+
width: 28px;
|
|
3723
|
+
height: 28px;
|
|
3724
|
+
border-radius: 6px;
|
|
3725
|
+
background: var(--muted);
|
|
3726
|
+
color: var(--muted-foreground);
|
|
3727
|
+
flex: none;
|
|
3728
|
+
}
|
|
3729
|
+
.pl-item[aria-selected="true"] .pl-icon-tile {
|
|
3730
|
+
color: var(--foreground);
|
|
3731
|
+
}
|
|
3732
|
+
.pl-enter-icon {
|
|
3733
|
+
display: flex;
|
|
3734
|
+
align-items: center;
|
|
3735
|
+
justify-content: center;
|
|
3736
|
+
width: 16px;
|
|
3737
|
+
height: 16px;
|
|
3738
|
+
color: var(--foreground);
|
|
3739
|
+
}
|
|
3740
|
+
.pl-eyebrow {
|
|
3741
|
+
font-family: var(--mono);
|
|
3742
|
+
font-size: 11px;
|
|
3743
|
+
line-height: 16px;
|
|
3744
|
+
letter-spacing: 0.04em;
|
|
3745
|
+
text-transform: uppercase;
|
|
3746
|
+
color: var(--muted-foreground);
|
|
3747
|
+
padding: 10px 8px 4px;
|
|
3599
3748
|
}
|
|
3600
3749
|
.pl-cli {
|
|
3601
3750
|
font-family: var(--mono);
|
|
3602
3751
|
font-size: 12px;
|
|
3603
3752
|
line-height: 16px;
|
|
3604
3753
|
color: var(--muted-foreground);
|
|
3754
|
+
padding: 2px 6px;
|
|
3755
|
+
border-radius: 6px;
|
|
3756
|
+
background: var(--background);
|
|
3757
|
+
box-shadow: 0 0 0 1px var(--border);
|
|
3605
3758
|
}
|
|
3606
3759
|
.pl-note {
|
|
3607
3760
|
margin: 0;
|
|
@@ -3612,6 +3765,74 @@
|
|
|
3612
3765
|
.pl-note--error {
|
|
3613
3766
|
color: var(--destructive);
|
|
3614
3767
|
}
|
|
3768
|
+
.pl-search {
|
|
3769
|
+
display: flex;
|
|
3770
|
+
align-items: center;
|
|
3771
|
+
gap: 10px;
|
|
3772
|
+
padding: 0 12px 0 16px;
|
|
3773
|
+
border-bottom: 1px solid var(--border);
|
|
3774
|
+
}
|
|
3775
|
+
.pl-search__icon {
|
|
3776
|
+
display: flex;
|
|
3777
|
+
color: var(--muted-foreground);
|
|
3778
|
+
}
|
|
3779
|
+
.pl-search > div {
|
|
3780
|
+
flex: 1 1 auto;
|
|
3781
|
+
}
|
|
3782
|
+
.pl-input.pl-input,
|
|
3783
|
+
.pl-input.pl-input:focus-visible {
|
|
3784
|
+
height: 52px;
|
|
3785
|
+
font-size: 16px;
|
|
3786
|
+
border: 0;
|
|
3787
|
+
outline: none;
|
|
3788
|
+
}
|
|
3789
|
+
.pl-kbd {
|
|
3790
|
+
font: 11px/16px var(--mono);
|
|
3791
|
+
color: var(--muted-foreground);
|
|
3792
|
+
padding: 0 6px;
|
|
3793
|
+
border-radius: 6px;
|
|
3794
|
+
box-shadow: 0 0 0 1px var(--border);
|
|
3795
|
+
}
|
|
3796
|
+
.pl-center .pl-scopes {
|
|
3797
|
+
align-self: flex-start;
|
|
3798
|
+
margin: 10px 16px 4px;
|
|
3799
|
+
}
|
|
3800
|
+
.pl-center .pl-list {
|
|
3801
|
+
flex: 1 1 auto;
|
|
3802
|
+
min-height: 0;
|
|
3803
|
+
max-height: none;
|
|
3804
|
+
padding: 4px 8px 8px;
|
|
3805
|
+
}
|
|
3806
|
+
.pl-center .pl-list .pl-item {
|
|
3807
|
+
border-bottom: 0;
|
|
3808
|
+
background: transparent;
|
|
3809
|
+
border-radius: 6px;
|
|
3810
|
+
min-height: 44px;
|
|
3811
|
+
padding: 6px 8px;
|
|
3812
|
+
}
|
|
3813
|
+
.pl-center .pl-list .pl-item > span:first-child {
|
|
3814
|
+
align-self: center;
|
|
3815
|
+
}
|
|
3816
|
+
.pl-foot {
|
|
3817
|
+
display: flex;
|
|
3818
|
+
align-items: center;
|
|
3819
|
+
gap: 12px;
|
|
3820
|
+
padding: 8px 16px;
|
|
3821
|
+
border-top: 1px solid var(--border);
|
|
3822
|
+
background: var(--background);
|
|
3823
|
+
}
|
|
3824
|
+
.pl-foot .pl-note {
|
|
3825
|
+
flex: 1 1 auto;
|
|
3826
|
+
min-width: 0;
|
|
3827
|
+
font-size: 12px;
|
|
3828
|
+
line-height: 16px;
|
|
3829
|
+
}
|
|
3830
|
+
.pl-keys {
|
|
3831
|
+
flex: none;
|
|
3832
|
+
font-size: 12px;
|
|
3833
|
+
line-height: 16px;
|
|
3834
|
+
color: var(--muted-foreground);
|
|
3835
|
+
}
|
|
3615
3836
|
/* the terminal's compose line (phone): one message field and Send, in the bottom bar */
|
|
3616
3837
|
.pl-compose {
|
|
3617
3838
|
display: flex;
|
|
@@ -31,6 +31,7 @@ import { repoRow } from './fixture.js';
|
|
|
31
31
|
import { sessionDefaults, setSessionDefaults } from './dock.js';
|
|
32
32
|
import { COPY } from './frame-copy.js';
|
|
33
33
|
import { askConfirm } from './dialogs.js';
|
|
34
|
+
import { icon } from './icons.js';
|
|
34
35
|
|
|
35
36
|
const P = COPY.page;
|
|
36
37
|
const roots = new WeakMap(); // host -> { root, sig }
|
|
@@ -104,10 +105,17 @@ async function setOnline(patch) {
|
|
|
104
105
|
setState({});
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
/* A card: a small uppercase eyebrow (icon + title) over the section's rows. The section ids and data-fr hooks are the contract. */
|
|
109
|
+
const section = (id, title, kids, action, iconName) => el('section', { class: 'fr-set__section', 'data-fr-section': id },
|
|
110
|
+
el('div', { class: 'fr-set__head' }, el('h2', { class: 'fr-set__title' }, iconName ? icon(iconName) : null, title), action || null),
|
|
109
111
|
kids);
|
|
110
112
|
|
|
113
|
+
/** A toggle row: the state text beside a Turn on / Turn off button (the same button as before the redesign). */
|
|
114
|
+
function switchRow({ textEl, on, disabled, attrs, onClick }) {
|
|
115
|
+
const toggle = button({ label: on ? P.turnOff : P.turnOn, variant: 'outline', size: 'sm', disabled: !!disabled, attrs, onClick });
|
|
116
|
+
return el('div', { class: 'fr-set__sw' }, el('div', { class: 'fr-set__sw-text' }, textEl), toggle);
|
|
117
|
+
}
|
|
118
|
+
|
|
111
119
|
/* ── the eight sections ────────────────────────────────────────────────────────────────────────────────────────── */
|
|
112
120
|
|
|
113
121
|
function reposSection(scope) {
|
|
@@ -142,7 +150,7 @@ function reposSection(scope) {
|
|
|
142
150
|
});
|
|
143
151
|
const kids = rows.length ? rows : [empty({ text: P.reposEmpty })];
|
|
144
152
|
if (addOpen) kids.push(addBoard());
|
|
145
|
-
return section('repositories', P.repos, kids, add);
|
|
153
|
+
return section('repositories', P.repos, kids, add, 'git-branch');
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
function loadAdd() {
|
|
@@ -173,7 +181,7 @@ function profilesSection() {
|
|
|
173
181
|
return section('profiles', P.profiles, [row({
|
|
174
182
|
title: P.profilesRow, meta: [P.profilesMeta], onOpen: open, openLabel: P.profilesOpen, trail: [more],
|
|
175
183
|
attrs: { 'data-fr-row': 'profiles' },
|
|
176
|
-
})]);
|
|
184
|
+
})], null, 'bot');
|
|
177
185
|
}
|
|
178
186
|
|
|
179
187
|
function sessionsSection() {
|
|
@@ -190,21 +198,21 @@ function sessionsSection() {
|
|
|
190
198
|
onCommit: (v) => { setSessionDefaults({ tier: v }); setState({}); },
|
|
191
199
|
attrs: { 'data-fr': 'default-model' },
|
|
192
200
|
});
|
|
193
|
-
return section('sessions', P.sessions, [el('div', { class: 'fr-set__fields' }, cli, model), el('p', { class: 'fr-set__note' }, P.sessionsNote)]);
|
|
201
|
+
return section('sessions', P.sessions, [el('div', { class: 'fr-set__fields fr-set__fields--grid' }, cli, model), el('p', { class: 'fr-set__note' }, P.sessionsNote)], null, 'square-terminal');
|
|
194
202
|
}
|
|
195
203
|
|
|
196
204
|
function validateSection(scope) {
|
|
197
205
|
const repo = scope === 'all' ? null : activeProduct();
|
|
198
206
|
const ws = wsOfRepo(repo);
|
|
199
|
-
if (!ws) return section('validate', P.validate, [empty({ text: P.validateNoRepo })]);
|
|
207
|
+
if (!ws) return section('validate', P.validate, [empty({ text: P.validateNoRepo })], null, 'check');
|
|
200
208
|
readConsent(ws);
|
|
201
209
|
const c = consentByWs.get(ws) || {};
|
|
202
210
|
const known = c.on === true || c.on === false;
|
|
203
211
|
const line = c.error ? P.validateUnread : !known ? '' : c.on ? P.validateOn : P.validateOff;
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
attrs: { 'data-fr': 'consent-toggle' }, onClick: () => setConsent(ws, !c.on),
|
|
207
|
-
}) :
|
|
212
|
+
const stateEl = el('span', { 'data-fr': 'consent-state', 'data-on': known ? String(c.on) : 'unread' }, line);
|
|
213
|
+
const toggleRow = known ? switchRow({
|
|
214
|
+
textEl: stateEl, label: line, on: c.on, attrs: { 'data-fr': 'consent-toggle' }, onClick: () => setConsent(ws, !c.on),
|
|
215
|
+
}) : el('div', { class: 'fr-set__sw' }, el('div', { class: 'fr-set__sw-text' }, stateEl));
|
|
208
216
|
// Validate itself is a tool, not a place: it opens as a tab in the dock, from here only
|
|
209
217
|
const open = button({
|
|
210
218
|
label: P.validateOpen, variant: 'outline', size: 'sm', attrs: { 'data-fr': 'open-validate' },
|
|
@@ -214,32 +222,31 @@ function validateSection(scope) {
|
|
|
214
222
|
const f = frame(); if (f) f.openDock();
|
|
215
223
|
},
|
|
216
224
|
});
|
|
217
|
-
return section('validate', P.validate, [
|
|
218
|
-
el('span', { 'data-fr': 'consent-state', 'data-on': known ? String(c.on) : 'unread' }, line), toggle, open)]);
|
|
225
|
+
return section('validate', P.validate, [toggleRow], open, 'check');
|
|
219
226
|
}
|
|
220
227
|
|
|
221
228
|
function peopleSection(scope) {
|
|
222
229
|
const repo = scope === 'all' ? null : activeProduct();
|
|
223
230
|
const ws = wsOfRepo(repo);
|
|
224
|
-
if (!ws) return section('people', P.people, [empty({ text: P.peopleNoRepo })]);
|
|
231
|
+
if (!ws) return section('people', P.people, [empty({ text: P.peopleNoRepo })], null, 'share-2');
|
|
225
232
|
readPeople(ws);
|
|
226
233
|
const c = peopleByWs.get(ws) || {};
|
|
227
234
|
const known = c.on === true || c.on === false;
|
|
228
235
|
const line = c.error ? c.error : !known ? '' : c.on ? P.peopleOn : P.peopleOff;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
attrs: { 'data-fr': 'people-calendar' }, onClick: () => setPeopleCalendar(ws, !c.on),
|
|
232
|
-
}) :
|
|
233
|
-
return section('people', P.people, [el('p', { class: 'fr-set__note' }, P.peopleNote),
|
|
234
|
-
el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'people-state', 'data-on': known ? String(c.on) : 'unread' }, line), toggle)]);
|
|
236
|
+
const stateEl = el('span', { 'data-fr': 'people-state', 'data-on': known ? String(c.on) : 'unread' }, line);
|
|
237
|
+
const toggleRow = known ? switchRow({
|
|
238
|
+
textEl: stateEl, label: line, on: c.on, attrs: { 'data-fr': 'people-calendar' }, onClick: () => setPeopleCalendar(ws, !c.on),
|
|
239
|
+
}) : el('div', { class: 'fr-set__sw' }, el('div', { class: 'fr-set__sw-text' }, stateEl));
|
|
240
|
+
return section('people', P.people, [toggleRow, el('p', { class: 'fr-set__note' }, P.peopleNote)], null, 'share-2');
|
|
235
241
|
}
|
|
236
242
|
|
|
237
243
|
function onlineSection() {
|
|
238
244
|
readOnline(false);
|
|
239
245
|
const d = online.data;
|
|
240
|
-
if (!d) return section('online', P.online, [el('p', { class: 'fr-set__note' }, online.error || '')]);
|
|
241
|
-
if (d.envHost) return section('online', P.online, [el('p', { class: 'fr-set__note', 'data-fr': 'online-state', 'data-on': 'env' }, P.onlineEnv(d.envHost))]);
|
|
242
|
-
const
|
|
246
|
+
if (!d) return section('online', P.online, [el('p', { class: 'fr-set__note' }, online.error || '')], null, 'globe');
|
|
247
|
+
if (d.envHost) return section('online', P.online, [el('p', { class: 'fr-set__note', 'data-fr': 'online-state', 'data-on': 'env' }, P.onlineEnv(d.envHost))], null, 'globe');
|
|
248
|
+
const stateP = el('p', { class: 'fr-set__note', 'data-fr': 'online-state', 'data-on': String(!!d.on) }, d.on ? P.onlineOn(d.host) : P.onlineOff);
|
|
249
|
+
const lines = [];
|
|
243
250
|
if (d.on) {
|
|
244
251
|
const r = d.running || {};
|
|
245
252
|
lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-tunnel' },
|
|
@@ -247,24 +254,24 @@ function onlineSection() {
|
|
|
247
254
|
if (d.keys && !d.keys.keys) lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-keys' }, d.keys.lastError ? P.onlineKeysError(d.keys.lastError) : P.onlineKeysWait));
|
|
248
255
|
if (d.lastRefusal) lines.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-refused', role: 'status' }, P.onlineRefused(d.lastRefusal.reason, d.lastRefusal.at)));
|
|
249
256
|
}
|
|
250
|
-
if (!d.canChange) return section('online', P.online, [...lines, el('p', { class: 'fr-set__note' }, P.onlineRemote)]);
|
|
257
|
+
if (!d.canChange) return section('online', P.online, [stateP, ...lines, el('p', { class: 'fr-set__note' }, P.onlineRemote)], null, 'globe');
|
|
251
258
|
const f = (key, label, placeholder) => field({
|
|
252
259
|
label, value: d[key] || '', placeholder,
|
|
253
260
|
onCommit: (v) => setOnline({ [key]: v }),
|
|
254
261
|
attrs: { 'data-fr': 'online-' + key, disabled: d.on ? true : undefined },
|
|
255
262
|
});
|
|
256
|
-
const fields = el('div', { class: 'fr-set__fields' },
|
|
263
|
+
const fields = el('div', { class: 'fr-set__fields fr-set__fields--grid' },
|
|
257
264
|
f('host', P.onlineHost, 'pmos.example.com'), f('tunnel', P.onlineTunnel, 'pmos'), f('team', P.onlineTeam, 'myteam.cloudflareaccess.com'),
|
|
258
265
|
f('audience', P.onlineAudience, ''), f('email', P.onlineEmail, 'you@example.com'));
|
|
259
266
|
const blocked = !d.on && (!d.complete ? P.onlineNeedFields : !d.cloudflared ? P.onlineNoCloudflared : null);
|
|
260
|
-
const
|
|
261
|
-
label: d.on ? P.
|
|
267
|
+
const toggleRow = switchRow({
|
|
268
|
+
textEl: stateP, label: d.on ? P.onlineOn(d.host) : P.onlineOff, on: !!d.on, disabled: !!blocked,
|
|
262
269
|
attrs: { 'data-fr': 'online-toggle' }, onClick: () => setOnline({ on: !d.on }),
|
|
263
270
|
});
|
|
264
|
-
const kids = [el('p', { class: 'fr-set__note' }, P.onlineIntro), fields
|
|
271
|
+
const kids = [toggleRow, ...lines, el('p', { class: 'fr-set__note' }, P.onlineIntro), fields];
|
|
265
272
|
if (blocked) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-blocked' }, blocked));
|
|
266
273
|
if (online.note) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'online-error', role: 'alert' }, online.note));
|
|
267
|
-
return section('online', P.online, kids);
|
|
274
|
+
return section('online', P.online, kids, null, 'globe');
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
function themeSection() {
|
|
@@ -273,7 +280,7 @@ function themeSection() {
|
|
|
273
280
|
items: [{ id: 'dark', label: P.dark }, { id: 'light', label: P.light }],
|
|
274
281
|
onChange: (id) => setState({ theme: id }),
|
|
275
282
|
attrs: { 'data-fr': 'theme' },
|
|
276
|
-
})]);
|
|
283
|
+
})], null, 'sun');
|
|
277
284
|
}
|
|
278
285
|
|
|
279
286
|
function readKitStatus(ws, force) {
|
|
@@ -362,10 +369,10 @@ function updatesSection(scope) {
|
|
|
362
369
|
const kids = [];
|
|
363
370
|
const kitPresent = !!(kit && kit.present);
|
|
364
371
|
const canUpdate = (cmp.appBehind && !app.fromCheckout) || (cmp.kitBehind && kitPresent);
|
|
365
|
-
kids.push(el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'app-version' }, P.updatesApp(app.version))));
|
|
372
|
+
kids.push(el('div', { class: 'fr-set__line fr-set__line--box' }, icon('panel-top'), el('span', { 'data-fr': 'app-version' }, P.updatesApp(app.version))));
|
|
366
373
|
if (cmp.appBehind && app.fromCheckout) kids.push(el('p', { class: 'fr-set__note', 'data-fr': 'app-checkout' }, P.updatesCheckout));
|
|
367
374
|
const kitText = scope === 'all' || !ws ? P.updatesNoRepo : !kit ? '' : kit.present ? P.updatesKit(kit.version) : P.updatesNoKit;
|
|
368
|
-
kids.push(el('div', { class: 'fr-set__line' }, el('span', { 'data-fr': 'kit-version' }, kitText)));
|
|
375
|
+
kids.push(el('div', { class: 'fr-set__line fr-set__line--box' }, icon('cloud'), el('span', { 'data-fr': 'kit-version' }, kitText)));
|
|
369
376
|
if (canUpdate) {
|
|
370
377
|
kids.push(el('div', { class: 'fr-set__line' }, button({
|
|
371
378
|
label: P.updateAllTo(upd.latest), variant: 'primary', size: 'sm', disabled: busy, attrs: { 'data-fr': 'update-all' },
|
|
@@ -378,11 +385,17 @@ function updatesSection(scope) {
|
|
|
378
385
|
if (upd.error) kids.push(el('p', { class: 'fr-set__note fr-set__error', role: 'alert', 'data-fr': 'update-error' }, upd.error));
|
|
379
386
|
if (upd.output) kids.push(el('pre', { class: 'fr-set__output', 'data-fr': 'update-output' }, upd.output));
|
|
380
387
|
kids.push(el('p', { class: 'fr-set__note' }, P.updatesNote));
|
|
381
|
-
|
|
382
|
-
|
|
388
|
+
return section('updates', P.updates, kids, null, 'arrow-down');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Check for updates sits in the page header; it asks about the current repository's kit. */
|
|
392
|
+
function checkButton(scope) {
|
|
393
|
+
const repo = scope === 'all' ? null : activeProduct();
|
|
394
|
+
const ws = wsOfRepo(repo) || wsOfRepo(activeProduct());
|
|
395
|
+
return button({
|
|
396
|
+
label: P.updatesCheck, variant: 'outline', size: 'sm', icon: 'refresh-cw', disabled: !!upd.phase || !ws,
|
|
383
397
|
attrs: { 'data-fr': 'check-updates' }, onClick: () => checkUpdates(ws),
|
|
384
398
|
});
|
|
385
|
-
return section('updates', P.updates, kids, check);
|
|
386
399
|
}
|
|
387
400
|
|
|
388
401
|
/* ── mount ────────────────────────────────────────────────────────────────────────────────────────────────────────── */
|
|
@@ -415,15 +428,18 @@ export function mountDestination(host, ctx) {
|
|
|
415
428
|
if (sig === r.sig || busy(r.root)) return;
|
|
416
429
|
r.sig = sig;
|
|
417
430
|
setChildren(r.root, [
|
|
418
|
-
el('header', { class: 'fr-set__intro' }, el('h1', { class: 'fr-set__h1' }, P.title),
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
431
|
+
el('header', { class: 'fr-set__intro' }, el('h1', { class: 'fr-set__h1' }, P.title), checkButton(scope)),
|
|
432
|
+
/* The DOM keeps the section order (the tests and the keyboard order read it); the grid in components.css places the
|
|
433
|
+
* left column (repositories, new sessions, profiles, theme) and the right (Validate, People, Online, updates). */
|
|
434
|
+
el('div', { class: 'fr-set__grid' },
|
|
435
|
+
reposSection(scope),
|
|
436
|
+
profilesSection(),
|
|
437
|
+
sessionsSection(),
|
|
438
|
+
validateSection(scope),
|
|
439
|
+
peopleSection(scope),
|
|
440
|
+
onlineSection(),
|
|
441
|
+
themeSection(),
|
|
442
|
+
updatesSection(scope)),
|
|
427
443
|
]);
|
|
428
444
|
}
|
|
429
445
|
|
|
@@ -17,6 +17,10 @@ export const COPY = {
|
|
|
17
17
|
command: 'Command', destination: 'Go to', initiative: 'Initiative', repository: 'Repository', session: 'Session',
|
|
18
18
|
tab: 'Tab', view: 'View', knowledge: 'Document', memory: 'Memory', guide: 'Guide', capture: 'Note', ask: 'Ask',
|
|
19
19
|
},
|
|
20
|
+
groups: {
|
|
21
|
+
destination: 'Go to', command: 'Commands', initiative: 'Initiatives', repository: 'Repositories', session: 'Sessions',
|
|
22
|
+
tab: 'Tabs', view: 'Views', knowledge: 'Knowledge', memory: 'Memory', guide: 'Guide', capture: 'Keep', ask: 'Keep',
|
|
23
|
+
},
|
|
20
24
|
hint: 'Type to search. Press Enter to open the first row.',
|
|
21
25
|
hintTyped: 'Type to search. Capture as note keeps what you typed.',
|
|
22
26
|
noMatch: (q) => `Nothing matches "${q}".`,
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
|
|
22
22
|
import { el } from './dom.js';
|
|
23
23
|
import { state, setState, FIXTURE } from './state.js';
|
|
24
|
-
import { sheet, field, tabs, row, badge, signal, toast, button, dialogText, ensureComponentsCss } from './primitives.js';
|
|
25
|
-
import { ensureSprite } from './icons.js';
|
|
24
|
+
import { sheet, field, tabs, row, badge, signal, toast, button, dialogText, ensureComponentsCss, narrowViewport } from './primitives.js';
|
|
25
|
+
import { ensureSprite, icon } from './icons.js';
|
|
26
26
|
import { pmGet, pmPost } from './http.js';
|
|
27
27
|
import { COPY } from './palette-copy.js';
|
|
28
28
|
|
|
@@ -215,15 +215,27 @@ function choose(it) {
|
|
|
215
215
|
try { it.run(); } catch (e) { console.warn('[palette] a row failed:', (e && e.message) || e); }
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/** Get the kind icon for a palette item. */
|
|
219
|
+
function kindIcon(kind) {
|
|
220
|
+
const iconMap = {
|
|
221
|
+
destination: 'arrow-right', command: 'code', initiative: 'target', repository: 'git-branch', session: 'square-terminal',
|
|
222
|
+
tab: 'panel-top', view: 'panel-top', knowledge: 'file-text', memory: 'history', guide: 'list', capture: 'text-quote', ask: 'bot',
|
|
223
|
+
};
|
|
224
|
+
return iconMap[kind] || 'circle';
|
|
225
|
+
}
|
|
226
|
+
|
|
218
227
|
function itemRow(it, i, id) {
|
|
219
228
|
const sigs = [];
|
|
220
229
|
if (it.cli) sigs.push(el('code', { class: 'pl-cli' }, it.cli));
|
|
221
230
|
else if (it.tag) sigs.push(signal({ text: it.tag }));
|
|
231
|
+
const lead = [el('span', { class: 'pl-icon-tile', 'aria-hidden': 'true' }, icon(kindIcon(it.kind)))];
|
|
232
|
+
const trail = i === sel ? [el('span', { class: 'pl-enter-icon', 'aria-hidden': 'true' }, icon('chevron-right'))] : [];
|
|
222
233
|
return row({
|
|
223
234
|
title: it.label,
|
|
224
235
|
meta: [it.hint].filter(Boolean),
|
|
225
236
|
signals: sigs,
|
|
226
|
-
lead
|
|
237
|
+
lead,
|
|
238
|
+
trail,
|
|
227
239
|
onOpen: () => choose(it),
|
|
228
240
|
openAttrs: { 'data-palette': 'item', 'data-kind': it.kind, tabindex: '-1' },
|
|
229
241
|
attrs: { class: 'pl-item', id, role: 'option', 'aria-selected': i === sel ? 'true' : 'false' },
|
|
@@ -238,8 +250,21 @@ function refresh() {
|
|
|
238
250
|
const sig = rows.map((it) => `${it.kind}|${it.label}|${it.hint || ''}|${it.cli || ''}`).join('\n') + '#' + sel;
|
|
239
251
|
if (sig !== ui.sig) {
|
|
240
252
|
ui.sig = sig;
|
|
241
|
-
|
|
242
|
-
const
|
|
253
|
+
// Build the list with eyebrow labels for groups
|
|
254
|
+
const grouped = [];
|
|
255
|
+
let lastKind = null;
|
|
256
|
+
let itemCount = 0;
|
|
257
|
+
for (const it of rows) {
|
|
258
|
+
if (it.kind !== lastKind) {
|
|
259
|
+
const groupName = COPY.groups[it.kind] || COPY.kinds[it.kind] || it.kind;
|
|
260
|
+
grouped.push(el('div', { class: 'pl-eyebrow', 'aria-hidden': 'true' }, groupName));
|
|
261
|
+
lastKind = it.kind;
|
|
262
|
+
}
|
|
263
|
+
grouped.push(itemRow(it, itemCount, ui.listId + '-' + itemCount));
|
|
264
|
+
itemCount++;
|
|
265
|
+
}
|
|
266
|
+
ui.list.replaceChildren(...grouped);
|
|
267
|
+
const cur = ui.list.querySelector('[role="option"][aria-selected="true"]');
|
|
243
268
|
ui.input.control.setAttribute('aria-activedescendant', cur ? cur.id : '');
|
|
244
269
|
if (cur && cur.scrollIntoView) cur.scrollIntoView({ block: 'nearest' });
|
|
245
270
|
}
|
|
@@ -262,7 +287,7 @@ function setScope(next) {
|
|
|
262
287
|
ui.input.control.focus();
|
|
263
288
|
}
|
|
264
289
|
|
|
265
|
-
const scopeTabs = () => tabs({ variant: 'line', items: SCOPES.map((id) => ({ id, label: COPY.scopes[id] })), value: scope, onChange: setScope, ariaLabel: COPY.scopeLabel, attrs: { 'data-palette': 'scopes' } });
|
|
290
|
+
const scopeTabs = () => tabs({ variant: narrowViewport() ? 'line' : 'segmented', items: SCOPES.map((id) => ({ id, label: COPY.scopes[id] })), value: scope, onChange: setScope, ariaLabel: COPY.scopeLabel, attrs: { 'data-palette': 'scopes', class: 'pl-scopes' } });
|
|
266
291
|
|
|
267
292
|
function onKey(e) {
|
|
268
293
|
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
|
@@ -282,11 +307,17 @@ function openPalette(deps) {
|
|
|
282
307
|
q = ''; scope = 'all'; sel = 0; noteKey = ''; pinned = null; busy = false;
|
|
283
308
|
const listId = 'pl-list-' + Math.random().toString(36).slice(2, 7);
|
|
284
309
|
const input = field({ label: COPY.inputLabel, hideLabel: true, placeholder: COPY.placeholder,
|
|
285
|
-
attrs: { 'data-palette': 'input', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': listId, 'aria-autocomplete': 'list', spellcheck: 'false' } });
|
|
310
|
+
attrs: { 'data-palette': 'input', role: 'combobox', 'aria-expanded': 'true', 'aria-controls': listId, 'aria-autocomplete': 'list', spellcheck: 'false', class: narrowViewport() ? '' : 'pl-input' } });
|
|
286
311
|
const list = el('div', { class: 'pl-list', role: 'listbox', id: listId, 'aria-label': COPY.title, 'data-palette': 'list' });
|
|
287
312
|
const note = el('p', { class: 'pl-note', role: 'status', 'data-palette': 'note' });
|
|
288
313
|
const scopes = scopeTabs();
|
|
289
|
-
|
|
314
|
+
|
|
315
|
+
// Center mode for palette on desktop, bottom sheet on phone
|
|
316
|
+
const mode = narrowViewport() ? 'bottom' : 'center';
|
|
317
|
+
const center = mode === 'center';
|
|
318
|
+
const search = center ? el('div', { class: 'pl-search' }, el('span', { class: 'pl-search__icon', 'aria-hidden': 'true' }, icon('search')), input, el('kbd', { class: 'pl-kbd', 'aria-hidden': 'true' }, 'Esc')) : input;
|
|
319
|
+
const foot = center ? el('div', { class: 'pl-foot' }, note, el('span', { class: 'pl-keys' }, COPY.keys)) : note;
|
|
320
|
+
const dlg = sheet({ mode, title: COPY.title, sub: COPY.sub, body: [search, scopes, list, foot], attrs: { 'data-palette': 'overlay', class: center ? 'pl-center' : '' },
|
|
290
321
|
onClose: () => { ui = null; if (state.paletteOpen) setState({ paletteOpen: false }); } });
|
|
291
322
|
ui = { sheet: dlg, input, list, note, scopes, deps, sig: null, listId };
|
|
292
323
|
input.control.addEventListener('input', () => { input.control.resetCommitted(input.control.value); q = input.control.value; sel = 0; pinned = null; if (ui) { ui.sig = null; refresh(); } });
|
|
@@ -394,21 +394,23 @@ export function narrowViewport() {
|
|
|
394
394
|
}
|
|
395
395
|
|
|
396
396
|
/**
|
|
397
|
-
* sheet({ mode: 'auto'|'side'|'bottom', title, sub, body: Node[], actions: Node[], onClose, attrs })
|
|
398
|
-
* A modal <dialog> in the layer: 420px at the side above 760px, a bottom sheet below. Escape and the backdrop close it.
|
|
397
|
+
* sheet({ mode: 'auto'|'side'|'bottom'|'center', title, sub, body: Node[], actions: Node[], onClose, attrs })
|
|
398
|
+
* A modal <dialog> in the layer: 420px at the side above 760px, a bottom sheet below, or centered. Escape and the backdrop close it.
|
|
399
399
|
* On a phone the sheet follows the on-screen keyboard through visualViewport. Returns { dialog, body, close, setHead }.
|
|
400
400
|
*/
|
|
401
401
|
export function sheet({ mode = 'auto', title, sub, body, actions, onClose, attrs } = {}) {
|
|
402
402
|
const m = mode === 'auto' ? (narrowViewport() ? 'bottom' : 'side') : mode;
|
|
403
403
|
const tid = nextId('pmc-sheet-t');
|
|
404
|
-
const titleEl = el('h2', { class: 'pmc-sheet__title', id: tid }, title || '');
|
|
405
|
-
const subEl = el('div', { class: 'pmc-sheet__sub' }, sub || '');
|
|
404
|
+
const titleEl = el('h2', { class: cx('pmc-sheet__title', m === 'center' && 'pmc-sheet__title--hidden'), id: tid }, title || '');
|
|
405
|
+
const subEl = el('div', { class: cx('pmc-sheet__sub', m === 'center' && 'pmc-sheet__sub--hidden') }, sub || '');
|
|
406
406
|
const bodyEl = el('div', { class: 'pmc-sheet__body' }, body || []);
|
|
407
407
|
let closed = false;
|
|
408
|
+
const headEl = m === 'center' ? null : el('div', { class: 'pmc-sheet__head' }, el('div', { class: 'pmc-sheet__heading' }, titleEl, subEl),
|
|
409
|
+
iconButton({ icon: 'x', ariaLabel: 'Close', onClick: () => close() }));
|
|
408
410
|
const dialog = el('dialog', { 'aria-modal': 'true', 'aria-labelledby': tid, tabindex: '-1', ...withClass(cx('pmc-sheet', 'pmc-sheet--' + m), attrs) },
|
|
409
411
|
m === 'bottom' ? el('div', { class: 'pmc-sheet__handle', 'aria-hidden': 'true' }) : null,
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
m === 'center' ? titleEl : headEl,
|
|
413
|
+
m === 'center' ? subEl : null,
|
|
412
414
|
bodyEl,
|
|
413
415
|
actions && actions.length ? el('div', { class: 'pmc-sheet__actions' }, actions) : null);
|
|
414
416
|
|