@threadlabs/looma 0.27.4 → 0.27.6

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.
@@ -1,8 +1,11 @@
1
1
  <template component="ui-callout">
2
2
  <defs>
3
- <prop name="tone" type="keyword" values="info, neutral, note, warning, success, danger" default="info"
4
- >Which kind of message the callout carries, shown by its icon and colour: info (the default) for information, neutral for a muted aside, warning for a
5
- caution, success for a confirmation, and danger for a harmful consequence. note remains as an alias for neutral.</prop>
3
+ <prop name="tone" type="keyword" values="info, neutral, note, tip, important, warning, caution, success, danger" default="info"
4
+ >Which kind of message the callout carries, shown by its icon and colour: info (the default) for information, note or neutral for a muted aside, tip for
5
+ advice, important for something the reader must not miss, warning for a caution, danger (or its alias caution) for a harmful consequence, and success for
6
+ a confirmation. These are the tones of VitePress, Docusaurus and GitHub alerts.</prop>
7
+ <prop name="heading" type="string" default=""
8
+ >A short heading above the message, such as "Before you upgrade". Optional; without it the message stands alone.</prop>
6
9
  </defs>
7
10
  <div role="note">
8
11
  <span class="icon" aria-hidden="true">
@@ -24,6 +27,16 @@
24
27
  <path d="M13 2v7h7"></path>
25
28
  <path d="m9.5 17.5 4-4 2 2-4 4H9.5v-2Z"></path>
26
29
  </svg>
30
+ <svg $if="$tone = 'tip'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
31
+ <path d="M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5"></path>
32
+ <path d="M9 18h6"></path>
33
+ <path d="M10 22h4"></path>
34
+ </svg>
35
+ <svg $if="$tone = 'important'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
36
+ <path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"></path>
37
+ <path d="M12 15h.01"></path>
38
+ <path d="M12 7v4"></path>
39
+ </svg>
27
40
  <svg $if="$tone = 'warning'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
28
41
  <path d="m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"></path>
29
42
  <path d="M12 9v4"></path>
@@ -33,13 +46,24 @@
33
46
  <circle cx="12" cy="12" r="10"></circle>
34
47
  <path d="m9 12 2 2 4-4"></path>
35
48
  </svg>
36
- <svg $if="$tone = 'danger'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
49
+ <svg
50
+ $if="$tone = 'danger' or $tone = 'caution'"
51
+ viewBox="0 0 24 24"
52
+ fill="none"
53
+ stroke="currentColor"
54
+ stroke-width="2"
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"
57
+ >
37
58
  <circle cx="12" cy="12" r="10"></circle>
38
59
  <path d="m15 9-6 6"></path>
39
60
  <path d="m9 9 6 6"></path>
40
61
  </svg>
41
62
  </span>
42
- <div class="content"><slot></slot></div>
63
+ <div class="content">
64
+ <strong class="heading" $if="$heading">{$heading}</strong>
65
+ <slot></slot>
66
+ </div>
43
67
  </div>
44
68
  <style>
45
69
  /* This component's hooks style the element they are set on, and never a nested one. */
@@ -147,6 +171,11 @@
147
171
  min-inline-size: 0;
148
172
  }
149
173
 
174
+ .heading {
175
+ display: block;
176
+ font-weight: var(--ui-font-semibold);
177
+ }
178
+
150
179
  :host([tone="neutral"]), :host([tone="note"]) {
151
180
  --_ui-selection-context-surface: initial;
152
181
  --_ui-selection-context-border: initial;
@@ -156,6 +185,18 @@
156
185
  --_callout-icon: var(--ui-text-secondary);
157
186
  }
158
187
 
188
+ :host([tone="tip"]) {
189
+ --_callout-surface: var(--ui-success-soft);
190
+ --_callout-border: var(--ui-success-solid);
191
+ --_callout-icon: var(--ui-success-solid);
192
+ }
193
+
194
+ :host([tone="important"]) {
195
+ --_callout-surface: var(--ui-accent-soft);
196
+ --_callout-border: var(--ui-accent-solid);
197
+ --_callout-icon: var(--ui-accent-solid);
198
+ }
199
+
159
200
  :host([tone="warning"]) {
160
201
  --_callout-surface: var(--ui-warning-soft);
161
202
  --_callout-border: var(--ui-warning-solid);
@@ -168,7 +209,7 @@
168
209
  --_callout-icon: var(--ui-success-solid);
169
210
  }
170
211
 
171
- :host([tone="danger"]) {
212
+ :host([tone="danger"]), :host([tone="caution"]) {
172
213
  --_callout-surface: var(--ui-danger-soft);
173
214
  --_callout-border: var(--ui-danger-solid);
174
215
  --_callout-icon: var(--ui-danger-solid);
@@ -14,6 +14,9 @@
14
14
  >In auto mode, arrow keys move focus and select the next panel. In manual mode, arrows move focus while Enter or Space selects a panel. Use manual when
15
15
  displaying a panel takes time.</prop>
16
16
  <prop name="stretch" type="boolean" default="false">Share the full width of the tab list equally between tabs instead of sizing each to its label.</prop>
17
+ <prop name="group" type="string" default=""
18
+ >Tabs with the same nonempty group share one selection: choosing a tab selects the tab with the same label in every other Tabs of that group in the
19
+ document, and the choice is remembered for the next visit. Use it for alternatives a reader picks once, such as a package manager or language.</prop>
17
20
  <state type="string" name="internalValue" value=""></state>
18
21
  <state name="tabs" type="list(object({ id: string, label: string }))" value="[]"></state>
19
22
  <event name="select" type="object">
@@ -1,6 +1,16 @@
1
1
  import { trackTrigger } from "../shared/trigger.js";
2
2
 
3
3
  let panelIds = 0;
4
+ const GROUP_EVENT = "looma-tabs-group";
5
+ const storageKey = (group) => `looma-tabs:${group}`;
6
+
7
+ // Storage can be missing or throw (private windows, blocked site data); the selection still syncs in the page.
8
+ function readStored(group) {
9
+ try { return globalThis.localStorage?.getItem(storageKey(group)) ?? null; } catch { return null; }
10
+ }
11
+ function writeStored(group, label) {
12
+ try { globalThis.localStorage?.setItem(storageKey(group), label); } catch { /* not remembered */ }
13
+ }
4
14
 
5
15
  /** Lists the authored panels (each labelled by its aria-label) as tabs and shows the selected one. */
6
16
  function connect(host) {
@@ -56,10 +66,30 @@ function connect(host) {
56
66
  syncButtons();
57
67
  });
58
68
 
69
+ const group = () => String(host.props.group.value || "");
70
+ const document = host.element.ownerDocument;
71
+ // A group shares a selection by tab label, since panel ids are unique in a document.
72
+ const selectLabel = (label) => {
73
+ const tab = host.state.tabs.find((candidate) => candidate.label === label);
74
+ if (!tab || tab.id === host.state.internalValue) return false;
75
+ host.state.internalValue = tab.id;
76
+ return true;
77
+ };
59
78
  const select = (button, how) => {
60
79
  const previousValue = host.state.internalValue || undefined;
61
80
  host.state.internalValue = button.value;
62
81
  host.dispatch("select", { value: button.value, previousValue, trigger: how });
82
+ const name = group();
83
+ const label = host.state.tabs.find((tab) => tab.id === button.value)?.label;
84
+ if (!name || label === undefined) return;
85
+ writeStored(name, label);
86
+ document.dispatchEvent(new CustomEvent(GROUP_EVENT, { detail: { group: name, label, source: host.element } }));
87
+ };
88
+ const onGroupSelect = (event) => {
89
+ const { group: name, label, source } = event.detail;
90
+ if (source === host.element || name !== group()) return;
91
+ const previousValue = host.state.internalValue || undefined;
92
+ if (selectLabel(label)) host.dispatch("select", { value: host.state.internalValue, previousValue, trigger: "programmatic" });
63
93
  };
64
94
  const onClick = (event) => {
65
95
  const button = event.target.closest?.('[role="tab"]');
@@ -104,7 +134,14 @@ function connect(host) {
104
134
  const tabObserver = new MutationObserver(syncButtons);
105
135
  tabObserver.observe(list, { childList: true });
106
136
  readPanels();
137
+ document.addEventListener(GROUP_EVENT, onGroupSelect);
138
+ // A remembered choice applies when the page does not choose a panel itself.
139
+ if (group() && !external) {
140
+ const stored = readStored(group());
141
+ if (stored !== null) selectLabel(stored);
142
+ }
107
143
  return () => {
144
+ document.removeEventListener(GROUP_EVENT, onGroupSelect);
108
145
  stop();
109
146
  stopTracking();
110
147
  observer.disconnect();