summaryrefslogtreecommitdiffhomepage
path: root/src/features/plugins/ui/PluginRow.svelte
blob: 457bd297399e442b3e388f06dd02041e87bfef0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<script lang="ts">
  import type { PluginNode } from "../logic/types";
  import { tierLabel } from "../logic/types";

  let {
    node,
    highlight,
    blockReason = null,
    busy = false,
    onToggle,
    onReload,
    onHover,
    onUnhover,
  }: {
    node: PluginNode;
    /** Hover-highlight state for this row: a direct dep, a dependent, or none. */
    highlight: "dep" | "dependent" | null;
    /**
     * Why the unload control is disabled (core-locked / last-provider), or
     * null when interactive. Missing rows are always disabled (no plugin).
     */
    blockReason?: string | null;
    /** Whether a mutation for this row is in flight (spinner in the controls). */
    busy?: boolean;
    onToggle: (node: PluginNode, checked: boolean) => void;
    onReload?: (node: PluginNode) => void;
    onHover: (id: string) => void;
    onUnhover: (id: string) => void;
  } = $props();

  // Missing rows have no checkbox at all — there is no plugin to load.
  const hasCheckbox = $derived(node.section !== "missing");
  // Core rows are LOCKED (no load/unload control) per §3/§5.
  const interactive = $derived(blockReason === null && !busy);
  const rowClass = $derived(
    [
      node.section === "missing" || node.locked || (!node.loaded && node.section === "discovered")
        ? "opacity-50"
        : "",
      highlight === "dep"
        ? "outline outline-1 outline-info bg-info/10"
        : highlight === "dependent"
          ? "outline outline-1 outline-warning bg-warning/10"
          : "",
    ]
      .filter(Boolean)
      .join(" "),
  );
</script>

<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<tr
  class={rowClass}
  onmouseover={() => onHover(node.id)}
  onmouseout={() => onUnhover(node.id)}
  onfocus={() => onHover(node.id)}
  onblur={() => onUnhover(node.id)}
>
  <td class="w-10">
    {#if hasCheckbox}
      {#if node.locked}
        <span
          class="inline-block w-4 text-center opacity-40"
          title={blockReason ?? "Core plugins cannot be unloaded"}
          aria-label="Locked (core)">🔒</span
        >
      {:else}
        <input
          type="checkbox"
          class="checkbox checkbox-sm"
          checked={node.loaded}
          disabled={!interactive}
          title={blockReason ?? undefined}
          aria-label={`${node.loaded ? "Unload" : "Load"} ${node.id}`}
          onchange={(e) => onToggle(node, e.currentTarget.checked)}
        />
      {/if}
    {/if}
  </td>
  <td>
    <div class="flex flex-col">
      <span class="font-medium">{node.name}</span>
      {#if node.reason !== null}
        <span class="text-xs text-error">{node.reason}</span>
      {/if}
      {#if node.dependsOn.length > 0}
        <span class="text-xs opacity-60">depends on: {node.dependsOn.join(", ")}</span>
      {/if}
    </div>
  </td>
  <td class="text-xs">
    <span class="badge badge-ghost badge-sm">{tierLabel(node.tier)}</span>
    {#if node.trust !== null}
      <span class="badge badge-ghost badge-sm">{node.trust}</span>
    {/if}
  </td>
  <td class="text-xs opacity-60">{node.version}</td>
  <td class="text-right">
    {#if node.loaded && onReload !== undefined}
      <button
        type="button"
        class="btn btn-ghost btn-xs"
        disabled={!interactive}
        title="Reload this plugin's code (picks up updates)"
        aria-label={`Reload ${node.id}`}
        onclick={() => onReload?.(node)}
      >
        {#if busy}
          <span class="loading loading-spinner loading-xs"></span>
        {:else}
          ↻
        {/if}
      </button>
    {/if}
  </td>
</tr>