@ai-ecoverse/slicc-spectrum 1.37.0 → 1.38.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/dist/slicc-ui.js CHANGED
@@ -10623,6 +10623,27 @@ var automation = {
10623
10623
  function automationOf(browser) {
10624
10624
  return browser.declined ? "declined" : browser.via ?? "none";
10625
10625
  }
10626
+ var tailnetStates = {
10627
+ off: ["neutral", "Off"],
10628
+ loading: ["info", "Loading"],
10629
+ "needs-login": ["notice", "Needs sign-in"],
10630
+ starting: ["info", "Connecting"],
10631
+ running: ["positive", "Connected"],
10632
+ failed: ["negative", "Failed"]
10633
+ };
10634
+ var tailnetOnly = {
10635
+ proxy: "Tailnet hosts only. Everything else goes through slicc-node.",
10636
+ extension: "Tailnet hosts only. Everything else goes through the SLICC extension.",
10637
+ page: "Tailnet hosts only. Everything else goes through this page\u2019s fetch.",
10638
+ none: "Tailnet hosts only. Nothing else is reachable."
10639
+ };
10640
+ function tailnetAddress(addresses) {
10641
+ return addresses.find((address) => address.startsWith("100.")) ?? addresses[0];
10642
+ }
10643
+ function exitNodeValue(tailnet) {
10644
+ if (tailnet.autoExitNode) return "auto";
10645
+ return tailnet.exitNodes?.find((node) => node.name === tailnet.exitNode)?.id ?? "none";
10646
+ }
10626
10647
  function networkLabel(status2) {
10627
10648
  return `Network: ${networkHealth[status2.health][1].toLowerCase()}`;
10628
10649
  }
@@ -10652,7 +10673,10 @@ var SliccNetwork = class extends ModelElement {
10652
10673
  checking: { state: true },
10653
10674
  copied: { state: true },
10654
10675
  uncopied: { state: true },
10655
- error: { state: true }
10676
+ error: { state: true },
10677
+ busy: { state: true },
10678
+ tailnetError: { state: true },
10679
+ tailnetCopied: { state: true }
10656
10680
  };
10657
10681
  constructor() {
10658
10682
  super();
@@ -10660,6 +10684,9 @@ var SliccNetwork = class extends ModelElement {
10660
10684
  this.copied = false;
10661
10685
  this.uncopied = false;
10662
10686
  this.error = "";
10687
+ this.busy = "";
10688
+ this.tailnetError = "";
10689
+ this.tailnetCopied = "";
10663
10690
  }
10664
10691
  static styles = [
10665
10692
  r(link_default),
@@ -10733,6 +10760,51 @@ var SliccNetwork = class extends ModelElement {
10733
10760
  flex: none;
10734
10761
  margin-block-start: var(--swc-spacing-50);
10735
10762
  }
10763
+ .warning {
10764
+ display: flex;
10765
+ align-items: flex-start;
10766
+ gap: var(--swc-spacing-100);
10767
+ padding: var(--swc-spacing-200);
10768
+ border-radius: var(--swc-corner-radius-medium-default);
10769
+ background: var(--swc-notice-subtle-background-color-default);
10770
+ --swc-icon-color: var(--swc-notice-visual-color);
10771
+ }
10772
+ .warning swc-icon-alert-triangle {
10773
+ flex: none;
10774
+ margin-block-start: var(--swc-spacing-50);
10775
+ }
10776
+ .node {
10777
+ display: grid;
10778
+ grid-template-columns: max-content minmax(0, 1fr);
10779
+ align-items: center;
10780
+ gap: var(--swc-spacing-100) var(--swc-spacing-200);
10781
+ margin: 0;
10782
+ }
10783
+ .node dt {
10784
+ color: var(--swc-neutral-subdued-content-color-default);
10785
+ }
10786
+ .node dd {
10787
+ margin: 0;
10788
+ }
10789
+ .key {
10790
+ display: flex;
10791
+ flex-wrap: wrap;
10792
+ align-items: center;
10793
+ gap: var(--swc-spacing-100);
10794
+ }
10795
+ .key sp-textfield {
10796
+ flex: 1 1 200px;
10797
+ }
10798
+ .exit {
10799
+ display: flex;
10800
+ flex-wrap: wrap;
10801
+ align-items: center;
10802
+ gap: var(--swc-spacing-100) var(--swc-spacing-200);
10803
+ }
10804
+ .exit sp-picker {
10805
+ width: 240px;
10806
+ max-width: 100%;
10807
+ }
10736
10808
  .command {
10737
10809
  display: flex;
10738
10810
  align-items: center;
@@ -10815,6 +10887,7 @@ var SliccNetwork = class extends ModelElement {
10815
10887
  ];
10816
10888
  subscribe(model) {
10817
10889
  this.error = "";
10890
+ this.tailnetError = "";
10818
10891
  return model.network ? [model.network.on("network", () => this.requestUpdate())] : [];
10819
10892
  }
10820
10893
  focus() {
@@ -10843,6 +10916,131 @@ var SliccNetwork = class extends ModelElement {
10843
10916
  this.uncopied = true;
10844
10917
  }
10845
10918
  }
10919
+ async #tailnet(action, run2) {
10920
+ if (this.busy) return;
10921
+ this.busy = action;
10922
+ this.tailnetError = "";
10923
+ try {
10924
+ await run2();
10925
+ } catch (error) {
10926
+ this.tailnetError = failure(error);
10927
+ } finally {
10928
+ this.busy = "";
10929
+ }
10930
+ }
10931
+ async #copyTailnet(what, value) {
10932
+ try {
10933
+ await globalThis.navigator.clipboard.writeText(value);
10934
+ this.tailnetCopied = what;
10935
+ this.tailnetError = "";
10936
+ } catch {
10937
+ this.tailnetCopied = "";
10938
+ this.tailnetError = "Couldn\u2019t copy. Select the text and copy it.";
10939
+ }
10940
+ }
10941
+ #waiting(action) {
10942
+ return this.busy !== "" && this.busy !== action;
10943
+ }
10944
+ #submitKey(event2, port) {
10945
+ event2.preventDefault();
10946
+ if (this.busy) return;
10947
+ const field = event2.currentTarget.querySelector(
10948
+ "sp-textfield"
10949
+ );
10950
+ const key2 = field.value.trim();
10951
+ field.value = "";
10952
+ if (key2) void this.#tailnet("key", () => port.submitAuthKey?.(key2));
10953
+ }
10954
+ #sendKey(event2) {
10955
+ event2.currentTarget.closest("form")?.requestSubmit();
10956
+ }
10957
+ #enterKey(event2) {
10958
+ if (event2.key === "Enter") this.#sendKey(event2);
10959
+ }
10960
+ async #signOut(port) {
10961
+ const ok = await confirm({
10962
+ title: "Sign out of Tailscale?",
10963
+ body: "This browser leaves your tailnet until you sign in again.",
10964
+ action: "Sign out",
10965
+ variant: "confirmation"
10966
+ });
10967
+ if (ok) await this.#tailnet("sign-out", () => port.logoutTailnet?.());
10968
+ }
10969
+ #copyRow(what, label, value) {
10970
+ const copied = this.tailnetCopied === what;
10971
+ return b`<dt>${label}</dt>
10972
+ <dd class="command" data-copy=${what}>
10973
+ <code>${value}</code>
10974
+ <swc-action-button size="s" quiet data-action=${`copy-${what}`} accessible-label=${copied ? "Copied" : `Copy ${label.toLowerCase()}`} @click=${() => this.#copyTailnet(what, value)}>
10975
+ <swc-icon-copy slot="icon"></swc-icon-copy>${copied ? "Copied" : "Copy"}
10976
+ </swc-action-button>
10977
+ </dd>`;
10978
+ }
10979
+ #signIn(port, tailnet) {
10980
+ return b`<p>Sign in to add this browser to your tailnet.</p>
10981
+ ${tailnet.loginUrl ? b`<p><a class="swc-Link" href=${tailnet.loginUrl} target="_blank" rel="noopener noreferrer" data-action="tailnet-sign-in">Sign in to Tailscale</a></p>` : A}
10982
+ ${port.submitAuthKey ? b`<p class="muted" id="tailnet-key">Or paste an auth key:</p>
10983
+ <form class="key" data-form="auth-key" @submit=${(event2) => this.#submitKey(event2, port)}>
10984
+ <sp-textfield size="m" type="password" autocomplete="off" label="Tailscale auth key" aria-labelledby="tailnet-key" placeholder="tskey-…" @keydown=${(event2) => this.#enterKey(event2)}></sp-textfield>
10985
+ <swc-button size="m" variant="secondary" data-action="submit-auth-key" ?pending=${this.busy === "key"} ?disabled=${this.#waiting("key")} @click=${(event2) => this.#sendKey(event2)}>Connect</swc-button>
10986
+ </form>` : A}`;
10987
+ }
10988
+ #running(port, status2, tailnet) {
10989
+ const address = tailnet.node ? tailnetAddress(tailnet.node.addresses) : void 0;
10990
+ const peers = tailnet.peers;
10991
+ return b`${tailnet.node ? b`<p>This browser is on your tailnet${peers === void 0 ? "" : ` with ${peers} other ${peers === 1 ? "device" : "devices"}`}.</p>
10992
+ <dl class="node">
10993
+ ${this.#copyRow("name", "Name", tailnet.node.name)}
10994
+ ${address ? this.#copyRow("address", "Address", address) : A}
10995
+ </dl>` : A}
10996
+ ${port.setExitNode && tailnet.exitNodes ? b`<div class="exit">
10997
+ <span id="exit-node">Exit node</span>
10998
+ <sp-picker size="m" label="Exit node" aria-labelledby="exit-node" data-action="exit-node" .value=${l(exitNodeValue(tailnet))} ?pending=${this.busy === "exit"} ?disabled=${this.#waiting("exit")} @change=${(event2) => this.#exit(event2, port)}>
10999
+ <sp-menu-item value="none">None</sp-menu-item>
11000
+ <sp-menu-item value="auto">Automatic</sp-menu-item>
11001
+ ${tailnet.exitNodes.map((node) => b`<sp-menu-item value=${node.id} ?disabled=${!node.online}>${node.name}${node.online ? A : b`<span slot="description">Offline</span>`}</sp-menu-item>`)}
11002
+ </sp-picker>
11003
+ </div>` : A}
11004
+ <p data-exit=${tailnet.exitNode ? "node" : "none"}>${tailnet.exitNode ? `Internet through the exit node ${tailnet.exitNode}.` : tailnetOnly[status2.route ?? "none"]}</p>
11005
+ ${tailnet.shieldsUp === false ? b`<div class="warning" data-shields="down"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>Shields down: peers on your tailnet can connect to this browser.</span></div>` : tailnet.shieldsUp ? b`<p class="muted" data-shields="up">Shields up: peers can’t connect to this browser.</p>` : A}
11006
+ ${port.logoutTailnet ? b`<div class="actions"><swc-button size="m" variant="secondary" data-action="tailnet-sign-out" ?pending=${this.busy === "sign-out"} ?disabled=${this.#waiting("sign-out")} @click=${() => this.#signOut(port)}>Sign out</swc-button></div>` : A}`;
11007
+ }
11008
+ #exit(event2, port) {
11009
+ const value = event2.target.value;
11010
+ void this.#tailnet("exit", () => port.setExitNode?.(value === "none" ? null : value));
11011
+ }
11012
+ #failed(port, tailnet) {
11013
+ return b`<div class="error" data-error="tailnet"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>${tailnet.detail ?? "Tailscale stopped."}</span></div>
11014
+ ${port.check ? b`<div class="actions"><swc-button size="m" variant="secondary" data-action="tailnet-retry" ?pending=${this.busy === "retry"} ?disabled=${this.#waiting("retry")} @click=${() => this.#tailnet("retry", () => port.check?.())}>Retry</swc-button></div>` : A}`;
11015
+ }
11016
+ #tailnetBody(port, status2, tailnet) {
11017
+ switch (tailnet.state) {
11018
+ case "off":
11019
+ return b`<p class="muted">Reach the devices on your tailnet, and send the rest through an exit node.</p>`;
11020
+ case "needs-login":
11021
+ return this.#signIn(port, tailnet);
11022
+ case "running":
11023
+ return this.#running(port, status2, tailnet);
11024
+ case "failed":
11025
+ return this.#failed(port, tailnet);
11026
+ default:
11027
+ return b`<p class="muted">${tailnet.detail ?? (tailnet.state === "loading" ? "Loading Tailscale\u2026" : "Joining your tailnet\u2026")}</p>`;
11028
+ }
11029
+ }
11030
+ #tailnetSection(port, status2) {
11031
+ const tailnet = status2.tailnet;
11032
+ if (!tailnet) return A;
11033
+ const [variant, label] = tailnetStates[tailnet.state];
11034
+ return b`<section aria-labelledby="tailnet" data-tailnet=${tailnet.state}>
11035
+ <div class="head">
11036
+ <h2 id="tailnet">Tailscale</h2>
11037
+ <swc-status-light variant=${variant}>${label}</swc-status-light>
11038
+ </div>
11039
+ ${port.setTailnet ? b`<sp-switch data-action="tailnet" .checked=${l(tailnet.state !== "off")} ?disabled=${!!this.busy} @change=${(event2) => this.#tailnet("switch", () => port.setTailnet?.(event2.target.checked))}>Use Tailscale</sp-switch>` : A}
11040
+ ${this.#tailnetBody(port, status2, tailnet)}
11041
+ ${this.tailnetError ? b`<div class="error" role="alert" data-error="tailnet-action"><swc-icon-alert-triangle size="s" aria-hidden="true"></swc-icon-alert-triangle><span>${this.tailnetError}</span></div>` : A}
11042
+ </section>`;
11043
+ }
10846
11044
  #state(status2) {
10847
11045
  const [variant, label] = networkHealth[status2.health];
10848
11046
  return b`<section aria-labelledby="state">
@@ -10900,6 +11098,7 @@ var SliccNetwork = class extends ModelElement {
10900
11098
  const status2 = port.status();
10901
11099
  return b`<div class="page" tabindex="-1">
10902
11100
  ${this.#state(status2)}
11101
+ ${this.#tailnetSection(port, status2)}
10903
11102
  ${this.#whole(status2)}
10904
11103
  ${this.#failures(status2)}
10905
11104
  </div>`;
@@ -15145,12 +15344,14 @@ var SliccApp = class extends ThemedElement {
15145
15344
  const restored = this.dock.api.getPanel("updates");
15146
15345
  if (restored) this.dock.reveal("updates");
15147
15346
  else this.show("updates");
15148
- if (!port.ready() && restored?.params?.boot !== false) {
15149
- this.dock.api.getPanel("updates")?.api.updateParameters({ boot: true });
15150
- this.#save();
15151
- }
15347
+ const panel = this.dock.api.getPanel("updates");
15348
+ if (!port.ready() && restored?.params?.boot !== false)
15349
+ panel?.api.updateParameters({ boot: true });
15350
+ else if (!restored) panel?.api.updateParameters({ boot: void 0 });
15351
+ this.#save();
15152
15352
  } else if (this.dock.api.getPanel("updates")?.params?.boot && port.ready() && !port.list().some((item2) => item2.state === "failed")) {
15153
15353
  this.dock.close("updates");
15354
+ this.#save();
15154
15355
  }
15155
15356
  }
15156
15357
  #factories(model) {
@@ -15436,7 +15637,7 @@ var SliccApp = class extends ThemedElement {
15436
15637
  const label = networkLabel(status2);
15437
15638
  const content = this.screen === "phone" ? networkIcon[status2.health] : b`<swc-status-light size="s" variant=${variant}>Network${status2.health === "ok" ? A : b`<span class="state"> ${state2.toLowerCase()}</span>`}</swc-status-light>`;
15438
15639
  return b`<swc-action-button id="network" quiet size="s" data-network data-health=${status2.health} accessible-label=${label} @click=${() => this.show("network")}>${content}</swc-action-button>
15439
- <swc-tooltip for="network" placement="bottom">${status2.detail ?? label}</swc-tooltip>`;
15640
+ <swc-tooltip for="network" placement="bottom">${status2.detail ?? label}${status2.tailnet?.state === "running" ? " Tailscale is connected." : ""}</swc-tooltip>`;
15440
15641
  }
15441
15642
  #headerButton(id, label, icon, click) {
15442
15643
  return b`<swc-action-button id=${id} size="s" quiet accessible-label=${label} @click=${click}>${icon}</swc-action-button>
@@ -16874,6 +17075,20 @@ var SliccChanges = class extends ModelElement {
16874
17075
  min-width: 0;
16875
17076
  user-select: all;
16876
17077
  }
17078
+ p code {
17079
+ display: inline-block;
17080
+ max-inline-size: 100%;
17081
+ font-size: var(--swc-font-size-50);
17082
+ color: inherit;
17083
+ }
17084
+ .summary {
17085
+ min-width: 0;
17086
+ overflow: hidden;
17087
+ text-overflow: ellipsis;
17088
+ }
17089
+ .bar swc-action-button {
17090
+ flex: none;
17091
+ }
16877
17092
  .repo {
16878
17093
  display: flex;
16879
17094
  gap: var(--swc-spacing-100);
@@ -17022,7 +17237,7 @@ var SliccChanges = class extends ModelElement {
17022
17237
  }
17023
17238
  #list(changes) {
17024
17239
  const grouped = groups(changes);
17025
- if (grouped.length < 2)
17240
+ if (grouped.length === 1 && !grouped[0][0])
17026
17241
  return b`<ul aria-label="Pending changes" @keydown=${this.#keydown}>${changes.map((change) => this.#row(change))}</ul>`;
17027
17242
  return b`<ul aria-label="Pending changes" @keydown=${this.#keydown}>${grouped.map(
17028
17243
  ([repo, items]) => b`<li data-repo=${repo}>
@@ -17065,7 +17280,7 @@ var SliccChanges = class extends ModelElement {
17065
17280
  if (unavailable) return this.#unavailable(unavailable);
17066
17281
  const changes = port?.changes() ?? [];
17067
17282
  return b`<div class="bar">
17068
- <span>${changes.length} pending ${changes.length === 1 ? "change" : "changes"}</span>
17283
+ <span class="summary">${changes.length} pending ${changes.length === 1 ? "change" : "changes"}</span>
17069
17284
  <span class="spacer"></span>
17070
17285
  ${changes.length ? b`<swc-action-button size="s" quiet @click=${() => this.#all("accept")}>Accept all</swc-action-button>
17071
17286
  <swc-action-button size="s" quiet @click=${() => this.#all("revert")}>Revert all</swc-action-button>` : A}
@@ -17090,9 +17305,26 @@ var SliccDiffPanel = class extends ThemedElement {
17090
17305
  flex: 1;
17091
17306
  min-height: 0;
17092
17307
  }
17093
- swc-action-group {
17308
+ :host {
17309
+ container-type: inline-size;
17310
+ }
17311
+ swc-action-group,
17312
+ .bar swc-action-button {
17094
17313
  flex: none;
17095
17314
  }
17315
+ .bar .path {
17316
+ min-width: 0;
17317
+ }
17318
+ @container (width < 480px) {
17319
+ .bar .label {
17320
+ position: absolute;
17321
+ inline-size: 1px;
17322
+ block-size: 1px;
17323
+ overflow: hidden;
17324
+ clip-path: inset(50%);
17325
+ white-space: nowrap;
17326
+ }
17327
+ }
17096
17328
  swc-action-button.selected {
17097
17329
  --swc-action-button-background-color-default: var(--swc-neutral-background-color-selected-default);
17098
17330
  --swc-action-button-background-color-hover: var(--swc-neutral-background-color-selected-hover);
@@ -17146,7 +17378,7 @@ var SliccDiffPanel = class extends ThemedElement {
17146
17378
  return b`<div class="bar">
17147
17379
  ${statusMark(change.status, false)}
17148
17380
  <span class="path" title=${this.path}>${this.path}</span>
17149
- <span>${statusLabels[change.status]}${by ? ` by ${by}` : ""}</span>
17381
+ <span class="label">${statusLabels[change.status]}${by ? ` by ${by}` : ""}</span>
17150
17382
  <span class="spacer"></span>
17151
17383
  ${change.before === null && change.after === null ? A : b`<swc-action-group size="s" compact accessible-label="Diff layout">
17152
17384
  ${layouts.map(
@@ -17159,11 +17391,11 @@ var SliccDiffPanel = class extends ThemedElement {
17159
17391
  >`
17160
17392
  )}
17161
17393
  </swc-action-group>`}
17162
- <swc-action-button size="s" quiet @click=${() => port?.accept(this.path)}>
17163
- <swc-icon-checkmark slot="icon"></swc-icon-checkmark>Accept
17394
+ <swc-action-button size="s" quiet data-action="accept" accessible-label="Accept" title="Accept" @click=${() => port?.accept(this.path)}>
17395
+ <swc-icon-checkmark slot="icon"></swc-icon-checkmark><span class="label">Accept</span>
17164
17396
  </swc-action-button>
17165
- <swc-action-button size="s" quiet @click=${() => this.#revert(port, change)}>
17166
- <swc-icon-revert slot="icon"></swc-icon-revert>Revert
17397
+ <swc-action-button size="s" quiet data-action="revert" accessible-label="Revert" title="Revert" @click=${() => this.#revert(port, change)}>
17398
+ <swc-icon-revert slot="icon"></swc-icon-revert><span class="label">Revert</span>
17167
17399
  </swc-action-button>
17168
17400
  </div>
17169
17401
  ${errorLine(this.error)}