@threadlabs/looma 0.27.3 → 0.27.5
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/components/ui-badge/ui-badge.html +3 -3
- package/components/ui-callout/ui-callout.html +47 -6
- package/components/ui-tabs/ui-tabs.html +3 -0
- package/components/ui-tabs/ui-tabs.js +37 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-badge.css +3 -3
- package/styles/ui-callout.css +18 -1
- package/vanilla/UiCallout.d.ts +2 -1
- package/vanilla/UiCallout.js +93 -26
- package/vanilla/UiTabs.d.ts +1 -0
- package/vanilla/UiTabs.js +8 -8
- package/vue/UiBadge.vue +3 -3
- package/vue/UiCallout.d.ts +12 -3
- package/vue/UiCallout.vue +102 -12
- package/vue/UiStatusMessage.d.ts +1 -1
- package/vue/UiTabs.d.ts +9 -0
- package/vue/UiTabs.vue +19 -1
- package/vue/chunks/UiBadge.js +2 -2
- package/vue/chunks/UiCallout.js +101 -34
- package/vue/chunks/UiTabs.js +23 -1
- package/vue/components.css +117 -102
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<prop name="variant" type="keyword" values="solid, subtle, outline, inverse, overlay" default="subtle"
|
|
4
4
|
>Emphasis. subtle sets toned text over a soft wash; solid fills with the tone and uses contrasting text; outline keeps the surface with a toned edge.
|
|
5
5
|
inverse uses a translucent contrasting surface and toned ink for a count or label on a filled control of that tone. Neutral looks the same in solid and
|
|
6
|
-
subtle. overlay inherits the containing filled control's text colour on a faint translucent fill,
|
|
6
|
+
subtle. overlay inherits the containing filled control's text colour on a faint translucent fill, with a quiet single-pixel boundary and no shadow. Its small circle is
|
|
7
7
|
20px. When space is constrained, the label truncates with an ellipsis while its full text remains available.</prop>
|
|
8
8
|
<prop name="tone" type="keyword" values="neutral, accent, info, success, warning, danger" default="neutral"
|
|
9
9
|
>Which colour the badge speaks in. neutral is a quiet grey for plain labels; accent draws the eye in the accent colour; info, success, warning, and danger
|
|
@@ -317,11 +317,11 @@
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
/* A count can keep its containing control's contrasting ink instead of reversing it.
|
|
320
|
-
The faint fill
|
|
320
|
+
The faint fill preserves text contrast; a quiet boundary separates the count from its control. */
|
|
321
321
|
:host([variant="overlay"]) {
|
|
322
322
|
--_badge-outline-surface: var(--ui-badge-surface, color-mix(in srgb, currentColor 6%, transparent));
|
|
323
323
|
color: var(--ui-badge-text, inherit);
|
|
324
|
-
border-color: var(--ui-badge-border, transparent);
|
|
324
|
+
border-color: var(--ui-badge-border, color-mix(in srgb, currentColor 20%, transparent));
|
|
325
325
|
box-shadow: none;
|
|
326
326
|
}
|
|
327
327
|
|
|
@@ -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,
|
|
5
|
-
|
|
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
|
|
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"
|
|
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();
|