@ds-mo/ui 1.0.2 → 1.1.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/components/ds-bar-nav-action.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-breadcrumb.js +1 -1
- package/dist/components/ds-button-group.js +1 -1
- package/dist/components/ds-button.js +1 -1
- package/dist/components/ds-card.js +1 -1
- package/dist/components/ds-card.js.map +1 -1
- package/dist/components/ds-checkbox.js +1 -1
- package/dist/components/ds-divider.js +1 -1
- package/dist/components/ds-empty-state.js +1 -1
- package/dist/components/ds-empty-state.js.map +1 -1
- package/dist/components/ds-fade.js +1 -1
- package/dist/components/ds-field.js +1 -1
- package/dist/components/ds-header.js +1 -1
- package/dist/components/ds-header.js.map +1 -1
- package/dist/components/ds-icon.js +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-label-wrap.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-radio-group.js +1 -1
- package/dist/components/ds-radio-group.js.map +1 -1
- package/dist/components/ds-scrollbar.js +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-surface.js +1 -1
- package/dist/components/ds-tab-group-nav.d.ts +11 -0
- package/dist/components/ds-tab-group-nav.js +2 -0
- package/dist/components/ds-tab-group-nav.js.map +1 -0
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-tab-group.js.map +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-tag.js +1 -1
- package/dist/components/ds-text.js +1 -1
- package/dist/components/ds-toast-provider.js +1 -1
- package/dist/components/ds-toggle-button-group.js +1 -1
- package/dist/components/ds-toggle-button.js +1 -1
- package/dist/components/ds-toggle.js +1 -1
- package/dist/components/ds-tooltip.js +1 -1
- package/dist/components/p-BQq26pt9.js +2 -0
- package/dist/components/p-BQq26pt9.js.map +1 -0
- package/dist/components/p-CMyQsk0K.js +2 -0
- package/dist/components/p-CMyQsk0K.js.map +1 -0
- package/dist/components/{p-D3CwGoIu.js → p-COsKOwZR.js} +2 -2
- package/dist/components/p-CpbnmEu1.js +2 -0
- package/dist/components/p-CpbnmEu1.js.map +1 -0
- package/dist/components/{p-DouEVPmi.js → p-Dd0GrKaF.js} +2 -2
- package/dist/components/{p-BvxKLqNx.js → p-H89PzqGU.js} +2 -2
- package/dist/components/{p-BYe8Rfdg.js → p-Vwlmhl4E.js} +2 -2
- package/dist/components/{p-C4p1fG67.js → p-woooYulR.js} +2 -2
- package/dist/types/.stencil/BarNav/BarNav.d.ts +7 -2
- package/dist/types/.stencil/BarNav/bar-nav-dom-utils.d.ts +1 -2
- package/dist/types/.stencil/TabGroupNav/TabGroupNav.d.ts +27 -0
- package/dist/types/components.d.ts +68 -0
- package/package.json +37 -7
- package/src/angular/index.ts +1 -0
- package/src/angular/proxies.ts +28 -0
- package/src/react/components.ts +46 -0
- package/src/react/ds-accordion.ts +27 -0
- package/src/react/ds-badge.ts +26 -0
- package/src/react/ds-banner.ts +27 -0
- package/src/react/ds-bar-nav-action.ts +27 -0
- package/src/react/ds-bar-nav.ts +33 -0
- package/src/react/ds-breadcrumb.ts +27 -0
- package/src/react/ds-button-group.ts +27 -0
- package/src/react/ds-button.ts +35 -0
- package/src/react/ds-card.ts +26 -0
- package/src/react/ds-checkbox.ts +27 -0
- package/src/react/ds-divider.ts +26 -0
- package/src/react/ds-empty-state.ts +26 -0
- package/src/react/ds-fade.ts +26 -0
- package/src/react/ds-field.ts +26 -0
- package/src/react/ds-header.ts +26 -0
- package/src/react/ds-icon.ts +26 -0
- package/src/react/ds-input.ts +33 -0
- package/src/react/ds-label-wrap.ts +26 -0
- package/src/react/ds-loader.ts +26 -0
- package/src/react/ds-menu.ts +33 -0
- package/src/react/ds-modal.ts +27 -0
- package/src/react/ds-pagination.ts +27 -0
- package/src/react/ds-panel-nav.ts +37 -0
- package/src/react/ds-radio-group.ts +27 -0
- package/src/react/ds-scrollbar.ts +26 -0
- package/src/react/ds-select.ts +27 -0
- package/src/react/ds-sidebar.ts +33 -0
- package/src/react/ds-skeleton.ts +26 -0
- package/src/react/ds-slider.ts +27 -0
- package/src/react/ds-surface.ts +26 -0
- package/src/react/ds-tab-group-nav.ts +27 -0
- package/src/react/ds-tab-group.ts +27 -0
- package/src/react/ds-table.ts +35 -0
- package/src/react/ds-tag.ts +35 -0
- package/src/react/ds-text.ts +26 -0
- package/src/react/ds-toast-provider.ts +26 -0
- package/src/react/ds-toggle-button-group.ts +27 -0
- package/src/react/ds-toggle-button.ts +27 -0
- package/src/react/ds-toggle.ts +27 -0
- package/src/react/ds-tooltip.ts +26 -0
- package/src/wc/components/Accordion/Accordion.tsx +144 -0
- package/src/wc/components/Badge/Badge.tsx +34 -0
- package/src/wc/components/Badge/index.ts +1 -0
- package/src/wc/components/Banner/Banner.tsx +148 -0
- package/src/wc/components/BarNav/BarNav.tsx +605 -0
- package/src/wc/components/BarNav/bar-nav-dom-utils.ts +23 -0
- package/src/wc/components/BarNav/bar-nav-tabs-menu-utils.ts +59 -0
- package/src/wc/components/BarNav/bar-nav-utils.ts +76 -0
- package/src/wc/components/BarNav/index.ts +1 -0
- package/src/wc/components/BarNavAction/BarNavAction.tsx +67 -0
- package/src/wc/components/BarNavAction/index.ts +1 -0
- package/src/wc/components/Breadcrumb/Breadcrumb.tsx +61 -0
- package/src/wc/components/Button/Button.tsx +158 -0
- package/src/wc/components/Button/index.ts +1 -0
- package/src/wc/components/ButtonGroup/ButtonGroup.tsx +110 -0
- package/src/wc/components/Card/Card.tsx +26 -0
- package/src/wc/components/Checkbox/Checkbox.tsx +60 -0
- package/src/wc/components/Checkbox/index.ts +1 -0
- package/src/wc/components/Divider/Divider.tsx +23 -0
- package/src/wc/components/Divider/index.ts +2 -0
- package/src/wc/components/EmptyState/EmptyState.tsx +32 -0
- package/src/wc/components/Fade/Fade.tsx +32 -0
- package/src/wc/components/Fade/index.ts +1 -0
- package/src/wc/components/Field/Field.tsx +28 -0
- package/src/wc/components/Header/Header.tsx +33 -0
- package/src/wc/components/Icon/Icon.tsx +112 -0
- package/src/wc/components/Icon/flag-icon-catalog.ts +68 -0
- package/src/wc/components/Icon/index.ts +1 -0
- package/src/wc/components/Icon/system-icon-catalog.ts +798 -0
- package/src/wc/components/Input/Input.tsx +100 -0
- package/src/wc/components/Input/index.ts +1 -0
- package/src/wc/components/LabelWrap/LabelWrap.tsx +38 -0
- package/src/wc/components/Loader/Loader.tsx +47 -0
- package/src/wc/components/Loader/index.ts +1 -0
- package/src/wc/components/Menu/Menu.tsx +340 -0
- package/src/wc/components/Modal/Modal.tsx +197 -0
- package/src/wc/components/Pagination/Pagination.tsx +97 -0
- package/src/wc/components/PanelNav/PanelNav.tsx +666 -0
- package/src/wc/components/PanelNav/index.ts +7 -0
- package/src/wc/components/PanelNav/panel-nav-utils.ts +124 -0
- package/src/wc/components/RadioGroup/RadioGroup.tsx +128 -0
- package/src/wc/components/RadioGroup/index.ts +1 -0
- package/src/wc/components/Scrollbar/Scrollbar.tsx +189 -0
- package/src/wc/components/Select/Select.tsx +195 -0
- package/src/wc/components/Sidebar/Sidebar.tsx +245 -0
- package/src/wc/components/Skeleton/Skeleton.tsx +64 -0
- package/src/wc/components/Skeleton/index.ts +1 -0
- package/src/wc/components/Slider/Slider.tsx +68 -0
- package/src/wc/components/Slider/index.ts +1 -0
- package/src/wc/components/Surface/Surface.tsx +81 -0
- package/src/wc/components/TabGroup/TabGroup.tsx +188 -0
- package/src/wc/components/TabGroup/tab-item-utils.ts +24 -0
- package/src/wc/components/TabGroupNav/TabGroupNav.tsx +188 -0
- package/src/wc/components/Table/Table.tsx +268 -0
- package/src/wc/components/Tag/Tag.tsx +135 -0
- package/src/wc/components/Tag/index.ts +1 -0
- package/src/wc/components/Text/Text.tsx +100 -0
- package/src/wc/components/Text/index.ts +1 -0
- package/src/wc/components/Toast/Toast.tsx +174 -0
- package/src/wc/components/Toast/toast-service.ts +60 -0
- package/src/wc/components/Toggle/Toggle.tsx +43 -0
- package/src/wc/components/Toggle/index.ts +1 -0
- package/src/wc/components/ToggleButton/ToggleButton.tsx +120 -0
- package/src/wc/components/ToggleButtonGroup/ToggleButtonGroup.tsx +118 -0
- package/src/wc/components/Tooltip/Tooltip.tsx +196 -0
- package/src/wc/components.d.ts +3028 -0
- package/dist/components/p-DV8pmt1m.js +0 -2
- package/dist/components/p-DV8pmt1m.js.map +0 -1
- package/dist/components/p-STnE_bmQ.js +0 -2
- package/dist/components/p-STnE_bmQ.js.map +0 -1
- /package/dist/components/{p-D3CwGoIu.js.map → p-COsKOwZR.js.map} +0 -0
- /package/dist/components/{p-DouEVPmi.js.map → p-Dd0GrKaF.js.map} +0 -0
- /package/dist/components/{p-BvxKLqNx.js.map → p-H89PzqGU.js.map} +0 -0
- /package/dist/components/{p-BYe8Rfdg.js.map → p-Vwlmhl4E.js.map} +0 -0
- /package/dist/components/{p-C4p1fG67.js.map → p-woooYulR.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { TextVariant, TextColor, TextColorToken, TextDecoration, TextAlign, LineTruncation, TextWrap, TextElement } from './Text';
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { Component, Prop, State, h, Host } from '@stencil/core';
|
|
2
|
+
import { subscribeToToasts, getToasts, toast as toastService, type ToastData, type ToastIntent } from './toast-service';
|
|
3
|
+
|
|
4
|
+
export type ToastPosition = 'top-center' | 'top-right' | 'bottom-center' | 'bottom-right';
|
|
5
|
+
|
|
6
|
+
const INTENT_BG: Record<ToastIntent, string> = {
|
|
7
|
+
neutral: 'var(--color-background-bold-neutral)',
|
|
8
|
+
brand: 'var(--color-background-bold-brand)',
|
|
9
|
+
positive: 'var(--color-background-bold-positive)',
|
|
10
|
+
negative: 'var(--color-background-bold-negative)',
|
|
11
|
+
warning: 'var(--color-background-bold-warning)',
|
|
12
|
+
caution: 'var(--color-background-bold-caution)',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
interface ToastItemState {
|
|
16
|
+
data: ToastData;
|
|
17
|
+
exiting: boolean;
|
|
18
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
19
|
+
remaining: number;
|
|
20
|
+
startedAt: number;
|
|
21
|
+
paused: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@Component({
|
|
25
|
+
tag: 'ds-toast-provider',
|
|
26
|
+
styleUrl: 'Toast.css',
|
|
27
|
+
scoped: true,
|
|
28
|
+
})
|
|
29
|
+
export class ToastProvider {
|
|
30
|
+
@Prop() position: ToastPosition = 'top-center';
|
|
31
|
+
|
|
32
|
+
@State() private items: ToastItemState[] = [];
|
|
33
|
+
|
|
34
|
+
private unsubscribe: (() => void) | null = null;
|
|
35
|
+
|
|
36
|
+
connectedCallback() {
|
|
37
|
+
this.unsubscribe = subscribeToToasts(() => this.syncFromStore());
|
|
38
|
+
this.syncFromStore();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
disconnectedCallback() {
|
|
42
|
+
this.items.forEach(item => { if (item.timer) clearTimeout(item.timer); });
|
|
43
|
+
this.unsubscribe?.();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private syncFromStore() {
|
|
47
|
+
const storeToasts = getToasts();
|
|
48
|
+
const existingIds = new Set(this.items.map(i => i.data.id));
|
|
49
|
+
const storeIds = new Set(storeToasts.map(t => t.id));
|
|
50
|
+
|
|
51
|
+
// Remove items no longer in store (instant, no animation since dismiss() already fires animation)
|
|
52
|
+
const kept = this.items.filter(i => storeIds.has(i.data.id));
|
|
53
|
+
|
|
54
|
+
// Add new items
|
|
55
|
+
const newItems: ToastItemState[] = storeToasts
|
|
56
|
+
.filter(t => !existingIds.has(t.id))
|
|
57
|
+
.map(data => {
|
|
58
|
+
const state: ToastItemState = {
|
|
59
|
+
data,
|
|
60
|
+
exiting: false,
|
|
61
|
+
timer: null,
|
|
62
|
+
remaining: data.duration,
|
|
63
|
+
startedAt: 0,
|
|
64
|
+
paused: false,
|
|
65
|
+
};
|
|
66
|
+
if (data.duration > 0) {
|
|
67
|
+
state.startedAt = Date.now();
|
|
68
|
+
state.timer = setTimeout(() => this.dismissItem(data.id), data.duration);
|
|
69
|
+
}
|
|
70
|
+
return state;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
this.items = [...kept, ...newItems];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private dismissItem(id: string) {
|
|
77
|
+
this.items = this.items.map(i =>
|
|
78
|
+
i.data.id === id ? { ...i, exiting: true, timer: null } : i
|
|
79
|
+
);
|
|
80
|
+
setTimeout(() => {
|
|
81
|
+
this.items = this.items.filter(i => i.data.id !== id);
|
|
82
|
+
toastService.dismiss(id);
|
|
83
|
+
}, 200);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private pauseItem(id: string) {
|
|
87
|
+
this.items = this.items.map(i => {
|
|
88
|
+
if (i.data.id !== id || i.paused || !i.timer) return i;
|
|
89
|
+
clearTimeout(i.timer);
|
|
90
|
+
const elapsed = Date.now() - i.startedAt;
|
|
91
|
+
return { ...i, paused: true, timer: null, remaining: Math.max(0, i.remaining - elapsed) };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private resumeItem(id: string) {
|
|
96
|
+
this.items = this.items.map(i => {
|
|
97
|
+
if (i.data.id !== id || !i.paused || i.remaining <= 0) return i;
|
|
98
|
+
const timer = setTimeout(() => this.dismissItem(id), i.remaining);
|
|
99
|
+
return { ...i, paused: false, timer, startedAt: Date.now() };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private getPositionStyle(): Record<string, string> {
|
|
104
|
+
const base: Record<string, string> = {
|
|
105
|
+
position: 'fixed',
|
|
106
|
+
zIndex: '9999',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
flexDirection: 'column',
|
|
109
|
+
gap: 'var(--dimension-space-075)',
|
|
110
|
+
pointerEvents: 'none',
|
|
111
|
+
padding: 'var(--dimension-space-100)',
|
|
112
|
+
maxWidth: '480px',
|
|
113
|
+
width: '100%',
|
|
114
|
+
boxSizing: 'border-box',
|
|
115
|
+
};
|
|
116
|
+
switch (this.position) {
|
|
117
|
+
case 'top-center':
|
|
118
|
+
return { ...base, top: '0', left: '50%', transform: 'translateX(-50%)', alignItems: 'center' };
|
|
119
|
+
case 'top-right':
|
|
120
|
+
return { ...base, top: '0', right: '0', alignItems: 'flex-end' };
|
|
121
|
+
case 'bottom-center':
|
|
122
|
+
return { ...base, bottom: '0', left: '50%', transform: 'translateX(-50%)', alignItems: 'center' };
|
|
123
|
+
case 'bottom-right':
|
|
124
|
+
return { ...base, bottom: '0', right: '0', alignItems: 'flex-end' };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
render() {
|
|
129
|
+
return (
|
|
130
|
+
<Host style={{ display: 'contents' }}>
|
|
131
|
+
<div
|
|
132
|
+
class="toast-container"
|
|
133
|
+
role="region"
|
|
134
|
+
aria-label="Notifications"
|
|
135
|
+
aria-live="polite"
|
|
136
|
+
style={this.getPositionStyle()}
|
|
137
|
+
>
|
|
138
|
+
{this.items.map(item => (
|
|
139
|
+
<div
|
|
140
|
+
key={item.data.id}
|
|
141
|
+
class={{ toast: true, 'toast--exit': item.exiting }}
|
|
142
|
+
role={item.data.intent === 'negative' ? 'alert' : 'status'}
|
|
143
|
+
style={{
|
|
144
|
+
backgroundColor: INTENT_BG[item.data.intent],
|
|
145
|
+
borderRadius: 'var(--dimension-radius-125)',
|
|
146
|
+
boxShadow: 'var(--effect-elevation-elevated-floating)',
|
|
147
|
+
display: 'flex',
|
|
148
|
+
alignItems: 'center',
|
|
149
|
+
gap: 'var(--dimension-space-075)',
|
|
150
|
+
padding: 'var(--dimension-space-075) var(--dimension-space-100)',
|
|
151
|
+
minHeight: 'var(--dimension-size-400)',
|
|
152
|
+
pointerEvents: 'auto',
|
|
153
|
+
color: 'var(--color-foreground-on-bold-background-primary)',
|
|
154
|
+
boxSizing: 'border-box',
|
|
155
|
+
}}
|
|
156
|
+
onMouseEnter={() => this.pauseItem(item.data.id)}
|
|
157
|
+
onMouseLeave={() => this.resumeItem(item.data.id)}
|
|
158
|
+
>
|
|
159
|
+
<span class="text-body-medium" style={{ flex: '1' }}>{item.data.message}</span>
|
|
160
|
+
<button
|
|
161
|
+
type="button"
|
|
162
|
+
class="toast-dismiss"
|
|
163
|
+
onClick={() => this.dismissItem(item.data.id)}
|
|
164
|
+
aria-label="Dismiss"
|
|
165
|
+
>
|
|
166
|
+
×
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
))}
|
|
170
|
+
</div>
|
|
171
|
+
</Host>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type ToastIntent = 'neutral' | 'brand' | 'positive' | 'negative' | 'warning' | 'caution';
|
|
2
|
+
|
|
3
|
+
export interface ToastData {
|
|
4
|
+
id: string;
|
|
5
|
+
message: string;
|
|
6
|
+
intent: ToastIntent;
|
|
7
|
+
duration: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ToastOptions {
|
|
11
|
+
message: string;
|
|
12
|
+
intent?: ToastIntent;
|
|
13
|
+
duration?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let toasts: ToastData[] = [];
|
|
17
|
+
const listeners = new Set<() => void>();
|
|
18
|
+
let idCounter = 0;
|
|
19
|
+
|
|
20
|
+
function emit() {
|
|
21
|
+
listeners.forEach(fn => fn());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function subscribeToToasts(fn: () => void): () => void {
|
|
25
|
+
listeners.add(fn);
|
|
26
|
+
return () => listeners.delete(fn);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getToasts(): ToastData[] {
|
|
30
|
+
return toasts;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const toast = {
|
|
34
|
+
show(options: ToastOptions): string {
|
|
35
|
+
const id = `ds-toast-${++idCounter}`;
|
|
36
|
+
toasts = [...toasts, { id, intent: 'neutral', duration: 4000, ...options }];
|
|
37
|
+
emit();
|
|
38
|
+
return id;
|
|
39
|
+
},
|
|
40
|
+
success(message: string, options?: Partial<ToastOptions>): string {
|
|
41
|
+
return toast.show({ message, intent: 'positive', ...options });
|
|
42
|
+
},
|
|
43
|
+
error(message: string, options?: Partial<ToastOptions>): string {
|
|
44
|
+
return toast.show({ message, intent: 'negative', ...options });
|
|
45
|
+
},
|
|
46
|
+
warning(message: string, options?: Partial<ToastOptions>): string {
|
|
47
|
+
return toast.show({ message, intent: 'warning', ...options });
|
|
48
|
+
},
|
|
49
|
+
info(message: string, options?: Partial<ToastOptions>): string {
|
|
50
|
+
return toast.show({ message, intent: 'neutral', ...options });
|
|
51
|
+
},
|
|
52
|
+
dismiss(id: string) {
|
|
53
|
+
toasts = toasts.filter(t => t.id !== id);
|
|
54
|
+
emit();
|
|
55
|
+
},
|
|
56
|
+
dismissAll() {
|
|
57
|
+
toasts = [];
|
|
58
|
+
emit();
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
@Component({
|
|
4
|
+
tag: 'ds-toggle',
|
|
5
|
+
styleUrl: 'Toggle.css',
|
|
6
|
+
shadow: true,
|
|
7
|
+
})
|
|
8
|
+
export class Toggle {
|
|
9
|
+
@Prop({ mutable: true }) checked: boolean = false;
|
|
10
|
+
@Prop() inactive: boolean = false;
|
|
11
|
+
|
|
12
|
+
@Event() dsChange!: EventEmitter<boolean>;
|
|
13
|
+
|
|
14
|
+
private handleClick = () => {
|
|
15
|
+
if (this.inactive) return;
|
|
16
|
+
this.checked = !this.checked;
|
|
17
|
+
this.dsChange.emit(this.checked);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
private handleKeyDown = (e: KeyboardEvent) => {
|
|
21
|
+
if (this.inactive) return;
|
|
22
|
+
if (e.key === ' ' || e.key === 'Enter') {
|
|
23
|
+
e.preventDefault();
|
|
24
|
+
this.handleClick();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
return (
|
|
30
|
+
<Host
|
|
31
|
+
role="switch"
|
|
32
|
+
aria-checked={String(this.checked)}
|
|
33
|
+
aria-disabled={this.inactive || undefined}
|
|
34
|
+
tabIndex={this.inactive ? -1 : 0}
|
|
35
|
+
class={{ toggle: true, checked: this.checked, inactive: this.inactive }}
|
|
36
|
+
onClick={this.handleClick}
|
|
37
|
+
onKeyDown={this.handleKeyDown}
|
|
38
|
+
>
|
|
39
|
+
<span class="thumb" />
|
|
40
|
+
</Host>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { } from './Toggle';
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
|
|
2
|
+
import type { IconSize } from '../Icon/Icon';
|
|
3
|
+
|
|
4
|
+
export type ToggleButtonElevation = 'none' | 'flat' | 'elevated' | 'floating';
|
|
5
|
+
export type ToggleButtonSize = 'md' | 'sm' | 'xs';
|
|
6
|
+
export type ToggleButtonBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
|
|
7
|
+
|
|
8
|
+
const ICON_SIZE: Record<ToggleButtonSize, IconSize> = { md: 'md', sm: 'sm', xs: 'xs' };
|
|
9
|
+
|
|
10
|
+
// Pixel size reference for internal layout (icon span font-size)
|
|
11
|
+
const ICON_PX: Record<ToggleButtonSize, number> = { md: 20, sm: 16, xs: 12 };
|
|
12
|
+
|
|
13
|
+
const TEXT_CLASS: Record<ToggleButtonSize, string> = {
|
|
14
|
+
md: 'text-body-medium-emphasis',
|
|
15
|
+
sm: 'text-body-small-emphasis',
|
|
16
|
+
xs: 'text-caption-emphasis',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
@Component({
|
|
20
|
+
tag: 'ds-toggle-button',
|
|
21
|
+
styleUrl: 'ToggleButton.css',
|
|
22
|
+
scoped: true,
|
|
23
|
+
})
|
|
24
|
+
export class ToggleButton {
|
|
25
|
+
/**
|
|
26
|
+
* Chrome level.
|
|
27
|
+
* none — ghost (transparent, no border, no shadow)
|
|
28
|
+
* flat — border only, transparent bg
|
|
29
|
+
* elevated — bg-primary + shadow [default]
|
|
30
|
+
* floating — bg-primary + FAB-strength shadow
|
|
31
|
+
*/
|
|
32
|
+
@Prop() elevation: ToggleButtonElevation = 'elevated';
|
|
33
|
+
|
|
34
|
+
/** Label text. */
|
|
35
|
+
@Prop() label: string | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Icon name for <ds-icon>. Set via JS property.
|
|
39
|
+
* @example el.icon = 'GridView';
|
|
40
|
+
*/
|
|
41
|
+
@Prop() icon: string | undefined;
|
|
42
|
+
|
|
43
|
+
/** Size: md (32px), sm (24px), xs (16px). */
|
|
44
|
+
@Prop() size: ToggleButtonSize = 'md';
|
|
45
|
+
|
|
46
|
+
/** Pill shape. */
|
|
47
|
+
@Prop() rounded: boolean = false;
|
|
48
|
+
|
|
49
|
+
/** Parent surface context — adjusts hover tokens for colored backgrounds. */
|
|
50
|
+
@Prop() background: ToggleButtonBackground | undefined;
|
|
51
|
+
|
|
52
|
+
/** Controlled pressed state. */
|
|
53
|
+
@Prop({ mutable: true }) pressed: boolean = false;
|
|
54
|
+
|
|
55
|
+
/** Disables interaction. */
|
|
56
|
+
@Prop() inactive: boolean = false;
|
|
57
|
+
|
|
58
|
+
@Prop({ attribute: 'aria-label' }) ariaLabel: string | undefined;
|
|
59
|
+
|
|
60
|
+
/** Emits the new pressed value (!pressed) on click. */
|
|
61
|
+
@Event() dsChange!: EventEmitter<boolean>;
|
|
62
|
+
|
|
63
|
+
private handleClick = () => {
|
|
64
|
+
if (this.inactive) return;
|
|
65
|
+
this.dsChange.emit(!this.pressed);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
render() {
|
|
69
|
+
const size = this.size;
|
|
70
|
+
const iconSize = ICON_SIZE[size];
|
|
71
|
+
const iconPx = ICON_PX[size];
|
|
72
|
+
const textClass = TEXT_CLASS[size];
|
|
73
|
+
const hasIcon = !!this.icon;
|
|
74
|
+
const hasLabel = !!this.label;
|
|
75
|
+
const isIconOnly = hasIcon && !hasLabel;
|
|
76
|
+
const isLabelOnly = hasLabel && !hasIcon;
|
|
77
|
+
const isIconAndLabel = hasIcon && hasLabel;
|
|
78
|
+
const bg = this.background;
|
|
79
|
+
const elevKey = this.elevation.charAt(0).toUpperCase() + this.elevation.slice(1);
|
|
80
|
+
|
|
81
|
+
const cls: Record<string, boolean> = {
|
|
82
|
+
'toggle-btn': true,
|
|
83
|
+
[`elevation${elevKey}`]: true,
|
|
84
|
+
sizeSM: size === 'sm',
|
|
85
|
+
sizeXS: size === 'xs',
|
|
86
|
+
rounded: this.rounded,
|
|
87
|
+
inactive: this.inactive,
|
|
88
|
+
pressed: this.pressed,
|
|
89
|
+
iconOnly: isIconOnly,
|
|
90
|
+
labelOnly: isLabelOnly,
|
|
91
|
+
iconAndLabel: isIconAndLabel,
|
|
92
|
+
onMedium: bg === 'medium',
|
|
93
|
+
onBold: bg === 'bold',
|
|
94
|
+
onStrong: bg === 'strong',
|
|
95
|
+
onAlwaysDark: bg === 'always-dark',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Host>
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
class={cls}
|
|
103
|
+
disabled={this.inactive}
|
|
104
|
+
aria-label={this.ariaLabel ?? this.label ?? 'toggle'}
|
|
105
|
+
aria-pressed={this.pressed}
|
|
106
|
+
onClick={this.handleClick}
|
|
107
|
+
>
|
|
108
|
+
{hasIcon && (
|
|
109
|
+
<span class="toggle-btn__icon" style={{ fontSize: `${iconPx}px` }}>
|
|
110
|
+
<ds-icon name={this.icon} size={iconSize} />
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
{hasLabel && (
|
|
114
|
+
<span class={`toggle-btn__label ${textClass}`}>{this.label}</span>
|
|
115
|
+
)}
|
|
116
|
+
</button>
|
|
117
|
+
</Host>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Component, Prop, Event, EventEmitter, h, Host } from '@stencil/core';
|
|
2
|
+
|
|
3
|
+
export type ToggleGroupElevation = 'none' | 'flat' | 'elevated' | 'floating';
|
|
4
|
+
export type ToggleGroupSize = 'md' | 'sm' | 'xs';
|
|
5
|
+
export type ToggleGroupBackground = 'faint' | 'medium' | 'bold' | 'strong' | 'always-dark';
|
|
6
|
+
|
|
7
|
+
export interface ToggleGroupItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Icon name for <ds-icon>. */
|
|
11
|
+
icon?: string;
|
|
12
|
+
inactive?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Component({
|
|
16
|
+
tag: 'ds-toggle-button-group',
|
|
17
|
+
styleUrl: 'ToggleButtonGroup.css',
|
|
18
|
+
scoped: true,
|
|
19
|
+
})
|
|
20
|
+
export class ToggleButtonGroup {
|
|
21
|
+
/**
|
|
22
|
+
* Array of item descriptors. Set via JS property.
|
|
23
|
+
* @example el.items = [{ id: 'list', label: 'List' }, { id: 'grid', icon: 'GridView' }];
|
|
24
|
+
*/
|
|
25
|
+
@Prop() items: ToggleGroupItem[] = [];
|
|
26
|
+
|
|
27
|
+
/** ID of the currently selected / pressed item. */
|
|
28
|
+
@Prop({ mutable: true }) value: string = '';
|
|
29
|
+
|
|
30
|
+
/** Chrome level for the group container. */
|
|
31
|
+
@Prop() elevation: ToggleGroupElevation = 'elevated';
|
|
32
|
+
|
|
33
|
+
/** Size passed to each ds-toggle-button. */
|
|
34
|
+
@Prop() size: ToggleGroupSize = 'md';
|
|
35
|
+
|
|
36
|
+
/** Pill shape for group + buttons. */
|
|
37
|
+
@Prop() rounded: boolean = false;
|
|
38
|
+
|
|
39
|
+
/** Parent surface context. */
|
|
40
|
+
@Prop() background: ToggleGroupBackground | undefined;
|
|
41
|
+
|
|
42
|
+
/** Emits the id of the newly selected item. */
|
|
43
|
+
@Event() dsChange!: EventEmitter<string>;
|
|
44
|
+
|
|
45
|
+
private handleItemChange(id: string) {
|
|
46
|
+
if (id === this.value) return;
|
|
47
|
+
this.value = id;
|
|
48
|
+
this.dsChange.emit(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
render() {
|
|
52
|
+
const items = this.items;
|
|
53
|
+
const count = items.length;
|
|
54
|
+
const elev = this.elevation;
|
|
55
|
+
const elevKey = elev.charAt(0).toUpperCase() + elev.slice(1);
|
|
56
|
+
const bg = this.background;
|
|
57
|
+
const isGhost = elev === 'none';
|
|
58
|
+
|
|
59
|
+
const groupCls: Record<string, boolean> = {
|
|
60
|
+
group: true,
|
|
61
|
+
[`group${elevKey}`]: true,
|
|
62
|
+
groupRounded: this.rounded,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Host>
|
|
67
|
+
<div class={groupCls} role="group">
|
|
68
|
+
{items.map((item, i) => {
|
|
69
|
+
const isFirst = i === 0;
|
|
70
|
+
const isLast = i === count - 1;
|
|
71
|
+
const isMid = !isFirst && !isLast;
|
|
72
|
+
const isPressed = item.id === this.value;
|
|
73
|
+
const isIconOnly = !!item.icon && !item.label;
|
|
74
|
+
const needsRoundedCorrection = this.rounded && !isIconOnly && this.size !== 'xs';
|
|
75
|
+
const sizeKey = this.size.toUpperCase() as 'MD' | 'SM' | 'XS';
|
|
76
|
+
|
|
77
|
+
const itemCls: Record<string, boolean> = {
|
|
78
|
+
item: true,
|
|
79
|
+
first: isFirst,
|
|
80
|
+
last: isLast,
|
|
81
|
+
middle: isMid,
|
|
82
|
+
itemShort: isGhost,
|
|
83
|
+
itemPressed: isGhost && isPressed,
|
|
84
|
+
[`roundedFirst${sizeKey}`]: !!(needsRoundedCorrection && isFirst),
|
|
85
|
+
[`roundedLast${sizeKey}`]: !!(needsRoundedCorrection && isLast),
|
|
86
|
+
[`roundedMid${sizeKey}`]: !!(needsRoundedCorrection && isMid),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const dividerCls: Record<string, boolean> = {
|
|
90
|
+
divider: true,
|
|
91
|
+
dividerGhost: isGhost,
|
|
92
|
+
dividerGhostXS: isGhost && this.size === 'xs',
|
|
93
|
+
dividerGhostSM: isGhost && this.size === 'sm',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return [
|
|
97
|
+
i > 0 && (
|
|
98
|
+
<div class={dividerCls} aria-hidden="true" />
|
|
99
|
+
),
|
|
100
|
+
<ds-toggle-button
|
|
101
|
+
class={itemCls}
|
|
102
|
+
elevation={elev}
|
|
103
|
+
size={this.size}
|
|
104
|
+
label={item.label}
|
|
105
|
+
icon={item.icon}
|
|
106
|
+
rounded={this.rounded}
|
|
107
|
+
background={bg}
|
|
108
|
+
pressed={isPressed}
|
|
109
|
+
inactive={item.inactive}
|
|
110
|
+
onDsChange={() => this.handleItemChange(item.id)}
|
|
111
|
+
/>,
|
|
112
|
+
];
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
</Host>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|