@ai-ecoverse/slicc-spectrum 1.37.1 → 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>`;
@@ -15438,7 +15637,7 @@ var SliccApp = class extends ThemedElement {
15438
15637
  const label = networkLabel(status2);
15439
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>`;
15440
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>
15441
- <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>`;
15442
15641
  }
15443
15642
  #headerButton(id, label, icon, click) {
15444
15643
  return b`<swc-action-button id=${id} size="s" quiet accessible-label=${label} @click=${click}>${icon}</swc-action-button>