pendentive 0.1.1
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/LICENSE +13 -0
- package/README.md +88 -0
- package/dist/components/avatar/Avatar.d.ts +26 -0
- package/dist/components/badge/Badge.d.ts +25 -0
- package/dist/components/button/Button.d.ts +51 -0
- package/dist/components/button/ButtonGroup.d.ts +36 -0
- package/dist/components/checkbox/Checkbox.d.ts +34 -0
- package/dist/components/command/Command.d.ts +59 -0
- package/dist/components/data/List.d.ts +40 -0
- package/dist/components/data/Table.d.ts +37 -0
- package/dist/components/data/Tree.d.ts +36 -0
- package/dist/components/feedback/Alert.d.ts +29 -0
- package/dist/components/feedback/ProgressBar.d.ts +32 -0
- package/dist/components/feedback/Skeleton.d.ts +20 -0
- package/dist/components/feedback/Spinner.d.ts +20 -0
- package/dist/components/feedback/Toast.d.ts +30 -0
- package/dist/components/field/ColorField.d.ts +33 -0
- package/dist/components/field/NumberField.d.ts +42 -0
- package/dist/components/field/TextArea.d.ts +35 -0
- package/dist/components/field/TextField.d.ts +39 -0
- package/dist/components/field/shared.d.ts +9 -0
- package/dist/components/field/textInput.css.d.ts +5 -0
- package/dist/components/hovercard/HoverCard.d.ts +35 -0
- package/dist/components/input/ChipInput.d.ts +31 -0
- package/dist/components/input/DatePicker.d.ts +66 -0
- package/dist/components/input/FileDropzone.d.ts +26 -0
- package/dist/components/layout/Card.d.ts +24 -0
- package/dist/components/layout/Section.d.ts +33 -0
- package/dist/components/layout/Separator.d.ts +18 -0
- package/dist/components/misc/ModeCard.d.ts +35 -0
- package/dist/components/navigation/Accordion.d.ts +42 -0
- package/dist/components/navigation/Breadcrumb.d.ts +25 -0
- package/dist/components/navigation/Pagination.d.ts +30 -0
- package/dist/components/navigation/Tabs.d.ts +38 -0
- package/dist/components/overlay/AlertDialog.d.ts +37 -0
- package/dist/components/overlay/ContextMenu.d.ts +19 -0
- package/dist/components/overlay/Dialog.d.ts +38 -0
- package/dist/components/overlay/Drawer.d.ts +40 -0
- package/dist/components/overlay/DropdownMenu.d.ts +55 -0
- package/dist/components/overlay/Popover.d.ts +33 -0
- package/dist/components/radio/RadioGroup.d.ts +37 -0
- package/dist/components/select/Combobox.d.ts +42 -0
- package/dist/components/select/Select.d.ts +48 -0
- package/dist/components/slider/RangeSlider.d.ts +43 -0
- package/dist/components/slider/Slider.d.ts +41 -0
- package/dist/components/switch/Switch.d.ts +35 -0
- package/dist/components/toggle/Toggle.d.ts +39 -0
- package/dist/components/toggle/ToggleGroup.d.ts +47 -0
- package/dist/components/tooltip/Tooltip.d.ts +22 -0
- package/dist/core/classNames.d.ts +6 -0
- package/dist/core/content.d.ts +2 -0
- package/dist/core/controller.d.ts +34 -0
- package/dist/core/dom.d.ts +14 -0
- package/dist/core/overlay.d.ts +14 -0
- package/dist/core/styleInjector.d.ts +12 -0
- package/dist/core/theme.d.ts +18 -0
- package/dist/core/types.d.ts +10 -0
- package/dist/index.cjs +4054 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +3934 -0
- package/dist/index.js.map +1 -0
- package/dist/linteau.global.js +397 -0
- package/dist/linteau.global.js.map +7 -0
- package/dist/styles/tokens.d.ts +33 -0
- package/dist/svg/Icon.d.ts +19 -0
- package/dist/svg/create-icon.d.ts +13 -0
- package/dist/svg/icons/alertTriangle.d.ts +1 -0
- package/dist/svg/icons/bell.d.ts +1 -0
- package/dist/svg/icons/calendar.d.ts +1 -0
- package/dist/svg/icons/check.d.ts +1 -0
- package/dist/svg/icons/chevronDown.d.ts +1 -0
- package/dist/svg/icons/chevronLeft.d.ts +1 -0
- package/dist/svg/icons/chevronRight.d.ts +1 -0
- package/dist/svg/icons/chevronUp.d.ts +1 -0
- package/dist/svg/icons/circle.d.ts +1 -0
- package/dist/svg/icons/copy.d.ts +1 -0
- package/dist/svg/icons/download.d.ts +1 -0
- package/dist/svg/icons/edit.d.ts +1 -0
- package/dist/svg/icons/externalLink.d.ts +1 -0
- package/dist/svg/icons/eye.d.ts +1 -0
- package/dist/svg/icons/eyeOff.d.ts +1 -0
- package/dist/svg/icons/filter.d.ts +1 -0
- package/dist/svg/icons/heart.d.ts +1 -0
- package/dist/svg/icons/home.d.ts +1 -0
- package/dist/svg/icons/index.d.ts +54 -0
- package/dist/svg/icons/info.d.ts +1 -0
- package/dist/svg/icons/link.d.ts +1 -0
- package/dist/svg/icons/loader.d.ts +1 -0
- package/dist/svg/icons/lock.d.ts +1 -0
- package/dist/svg/icons/mail.d.ts +1 -0
- package/dist/svg/icons/menu.d.ts +1 -0
- package/dist/svg/icons/minus.d.ts +1 -0
- package/dist/svg/icons/moon.d.ts +1 -0
- package/dist/svg/icons/moreHorizontal.d.ts +1 -0
- package/dist/svg/icons/phone.d.ts +1 -0
- package/dist/svg/icons/plus.d.ts +1 -0
- package/dist/svg/icons/refreshCw.d.ts +1 -0
- package/dist/svg/icons/search.d.ts +1 -0
- package/dist/svg/icons/settings.d.ts +1 -0
- package/dist/svg/icons/star.d.ts +1 -0
- package/dist/svg/icons/sun.d.ts +1 -0
- package/dist/svg/icons/trash.d.ts +1 -0
- package/dist/svg/icons/upload.d.ts +1 -0
- package/dist/svg/icons/user.d.ts +1 -0
- package/dist/svg/icons/x.d.ts +1 -0
- package/dist/svg/index.cjs +380 -0
- package/dist/svg/index.cjs.map +1 -0
- package/dist/svg/index.d.ts +4 -0
- package/dist/svg/index.js +352 -0
- package/dist/svg/index.js.map +1 -0
- package/dist/svg/types.d.ts +26 -0
- package/package.json +81 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4054 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Accordion: () => Accordion,
|
|
24
|
+
Alert: () => Alert,
|
|
25
|
+
AlertDialog: () => AlertDialog,
|
|
26
|
+
Avatar: () => Avatar,
|
|
27
|
+
Badge: () => Badge,
|
|
28
|
+
Breadcrumb: () => Breadcrumb,
|
|
29
|
+
Button: () => Button,
|
|
30
|
+
ButtonGroup: () => ButtonGroup,
|
|
31
|
+
Card: () => Card,
|
|
32
|
+
Checkbox: () => Checkbox,
|
|
33
|
+
ChipInput: () => ChipInput,
|
|
34
|
+
ColorField: () => ColorField,
|
|
35
|
+
Combobox: () => Combobox,
|
|
36
|
+
Command: () => Command,
|
|
37
|
+
ContextMenu: () => ContextMenu,
|
|
38
|
+
DatePicker: () => DatePicker,
|
|
39
|
+
Dialog: () => Dialog,
|
|
40
|
+
Drawer: () => Drawer,
|
|
41
|
+
DropdownMenu: () => DropdownMenu,
|
|
42
|
+
FileDropzone: () => FileDropzone,
|
|
43
|
+
HoverCard: () => HoverCard,
|
|
44
|
+
Icon: () => Icon,
|
|
45
|
+
List: () => List,
|
|
46
|
+
ModeCard: () => ModeCard,
|
|
47
|
+
NumberField: () => NumberField,
|
|
48
|
+
Pagination: () => Pagination,
|
|
49
|
+
Popover: () => Popover,
|
|
50
|
+
ProgressBar: () => ProgressBar,
|
|
51
|
+
RadioGroup: () => RadioGroup,
|
|
52
|
+
RangeSlider: () => RangeSlider,
|
|
53
|
+
Section: () => Section,
|
|
54
|
+
Select: () => Select,
|
|
55
|
+
Separator: () => Separator,
|
|
56
|
+
Skeleton: () => Skeleton,
|
|
57
|
+
Slider: () => Slider,
|
|
58
|
+
Spinner: () => Spinner,
|
|
59
|
+
Switch: () => Switch,
|
|
60
|
+
Table: () => Table,
|
|
61
|
+
Tabs: () => Tabs,
|
|
62
|
+
TextArea: () => TextArea,
|
|
63
|
+
TextField: () => TextField,
|
|
64
|
+
Toggle: () => Toggle,
|
|
65
|
+
ToggleGroup: () => ToggleGroup,
|
|
66
|
+
Tooltip: () => Tooltip,
|
|
67
|
+
Tree: () => Tree,
|
|
68
|
+
accordionCss: () => accordionCss,
|
|
69
|
+
alertCss: () => alertCss,
|
|
70
|
+
avatarCss: () => avatarCss,
|
|
71
|
+
badgeCss: () => badgeCss,
|
|
72
|
+
breadcrumbCss: () => breadcrumbCss,
|
|
73
|
+
buttonCss: () => buttonCss,
|
|
74
|
+
buttonGroupCss: () => buttonGroupCss,
|
|
75
|
+
calendarCss: () => calendarCss,
|
|
76
|
+
cardCss: () => cardCss,
|
|
77
|
+
checkboxCss: () => checkboxCss,
|
|
78
|
+
chipInputCss: () => chipInputCss,
|
|
79
|
+
comboboxCss: () => comboboxCss,
|
|
80
|
+
commandCss: () => commandCss,
|
|
81
|
+
createToastManager: () => createToastManager,
|
|
82
|
+
dialogCss: () => dialogCss,
|
|
83
|
+
drawerCss: () => drawerCss,
|
|
84
|
+
dropzoneCss: () => dropzoneCss,
|
|
85
|
+
getTheme: () => getTheme,
|
|
86
|
+
hoverCardCss: () => hoverCardCss,
|
|
87
|
+
icons: () => icons,
|
|
88
|
+
isBrowser: () => isBrowser,
|
|
89
|
+
listCss: () => listCss,
|
|
90
|
+
menuCss: () => menuCss,
|
|
91
|
+
modeCardCss: () => modeCardCss,
|
|
92
|
+
onThemeChange: () => onThemeChange,
|
|
93
|
+
paginationCss: () => paginationCss,
|
|
94
|
+
popoverCss: () => popoverCss,
|
|
95
|
+
progressBarCss: () => progressBarCss,
|
|
96
|
+
radioCss: () => radioCss,
|
|
97
|
+
rangeSliderCss: () => rangeSliderCss,
|
|
98
|
+
renderMenuEntries: () => renderMenuEntries,
|
|
99
|
+
resetTheme: () => resetTheme,
|
|
100
|
+
sectionCss: () => sectionCss,
|
|
101
|
+
selectCss: () => selectCss,
|
|
102
|
+
separatorCss: () => separatorCss,
|
|
103
|
+
setTheme: () => setTheme,
|
|
104
|
+
skeletonCss: () => skeletonCss,
|
|
105
|
+
sliderCss: () => sliderCss,
|
|
106
|
+
spinnerCss: () => spinnerCss,
|
|
107
|
+
swatchCss: () => swatchCss,
|
|
108
|
+
switchCss: () => switchCss,
|
|
109
|
+
tableCss: () => tableCss,
|
|
110
|
+
tabsCss: () => tabsCss,
|
|
111
|
+
toast: () => toast,
|
|
112
|
+
toastCss: () => toastCss,
|
|
113
|
+
toggleCss: () => toggleCss,
|
|
114
|
+
toggleGroupCss: () => toggleGroupCss,
|
|
115
|
+
tooltipCss: () => tooltipCss,
|
|
116
|
+
treeCss: () => treeCss
|
|
117
|
+
});
|
|
118
|
+
module.exports = __toCommonJS(index_exports);
|
|
119
|
+
|
|
120
|
+
// src/core/dom.ts
|
|
121
|
+
function isBrowser() {
|
|
122
|
+
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
123
|
+
}
|
|
124
|
+
function assertDom(componentName) {
|
|
125
|
+
if (!isBrowser()) throw new Error(`[linteau] "${componentName}" requires a DOM environment (a browser, or Node/Bun with a DOM shim such as 'happy-dom' or 'linkedom'). No 'document' was found.`);
|
|
126
|
+
}
|
|
127
|
+
function el(tag, className) {
|
|
128
|
+
const node = document.createElement(tag);
|
|
129
|
+
if (className) node.className = className;
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// src/core/controller.ts
|
|
134
|
+
function createEmitter() {
|
|
135
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
136
|
+
return {
|
|
137
|
+
on(event, listener) {
|
|
138
|
+
if (!listeners.has(event)) listeners.set(event, /* @__PURE__ */ new Set());
|
|
139
|
+
listeners.get(event).add(listener);
|
|
140
|
+
return () => listeners.get(event)?.delete(listener);
|
|
141
|
+
},
|
|
142
|
+
off(event, listener) {
|
|
143
|
+
listeners.get(event)?.delete(listener);
|
|
144
|
+
},
|
|
145
|
+
emit(event, payload) {
|
|
146
|
+
listeners.get(event)?.forEach((listener) => listener(payload));
|
|
147
|
+
},
|
|
148
|
+
clear() {
|
|
149
|
+
listeners.clear();
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function attachController(element, api) {
|
|
154
|
+
return Object.assign(element, api);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/styles/tokens.ts
|
|
158
|
+
var defaultTheme = {
|
|
159
|
+
background: "oklch(0.09 0 0)",
|
|
160
|
+
foreground: "oklch(0.94 0 0)",
|
|
161
|
+
card: "oklch(0.12 0 0)",
|
|
162
|
+
cardForeground: "oklch(0.94 0 0)",
|
|
163
|
+
primary: "oklch(0.92 0 0)",
|
|
164
|
+
primaryForeground: "oklch(0.1 0 0)",
|
|
165
|
+
secondary: "oklch(0.18 0 0)",
|
|
166
|
+
secondaryForeground: "oklch(0.9 0 0)",
|
|
167
|
+
muted: "oklch(0.16 0 0)",
|
|
168
|
+
mutedForeground: "oklch(0.6 0 0)",
|
|
169
|
+
accent: "oklch(0.2 0 0)",
|
|
170
|
+
accentForeground: "oklch(0.94 0 0)",
|
|
171
|
+
destructive: "oklch(0.58 0.22 27)",
|
|
172
|
+
destructiveForeground: "oklch(0.97 0 0)",
|
|
173
|
+
success: "oklch(0.62 0.16 145)",
|
|
174
|
+
successForeground: "oklch(0.97 0 0)",
|
|
175
|
+
warning: "oklch(0.75 0.15 85)",
|
|
176
|
+
warningForeground: "oklch(0.15 0 0)",
|
|
177
|
+
border: "oklch(0.22 0 0)",
|
|
178
|
+
input: "oklch(0.22 0 0)",
|
|
179
|
+
ring: "oklch(0.7 0 0)",
|
|
180
|
+
radius: "0.625rem",
|
|
181
|
+
radiusSm: "calc(0.625rem - 4px)",
|
|
182
|
+
radiusMd: "calc(0.625rem - 2px)",
|
|
183
|
+
radiusLg: "0.625rem",
|
|
184
|
+
radiusXl: "calc(0.625rem + 4px)"
|
|
185
|
+
};
|
|
186
|
+
function buildTokensCss(tokens) {
|
|
187
|
+
return `:root {
|
|
188
|
+
--linteau-background: ${tokens.background};
|
|
189
|
+
--linteau-foreground: ${tokens.foreground};
|
|
190
|
+
--linteau-card: ${tokens.card};
|
|
191
|
+
--linteau-card-foreground: ${tokens.cardForeground};
|
|
192
|
+
--linteau-primary: ${tokens.primary};
|
|
193
|
+
--linteau-primary-foreground: ${tokens.primaryForeground};
|
|
194
|
+
--linteau-secondary: ${tokens.secondary};
|
|
195
|
+
--linteau-secondary-foreground: ${tokens.secondaryForeground};
|
|
196
|
+
--linteau-muted: ${tokens.muted};
|
|
197
|
+
--linteau-muted-foreground: ${tokens.mutedForeground};
|
|
198
|
+
--linteau-accent: ${tokens.accent};
|
|
199
|
+
--linteau-accent-foreground: ${tokens.accentForeground};
|
|
200
|
+
--linteau-destructive: ${tokens.destructive};
|
|
201
|
+
--linteau-destructive-foreground: ${tokens.destructiveForeground};
|
|
202
|
+
--linteau-success: ${tokens.success};
|
|
203
|
+
--linteau-success-foreground: ${tokens.successForeground};
|
|
204
|
+
--linteau-warning: ${tokens.warning};
|
|
205
|
+
--linteau-warning-foreground: ${tokens.warningForeground};
|
|
206
|
+
--linteau-border: ${tokens.border};
|
|
207
|
+
--linteau-input: ${tokens.input};
|
|
208
|
+
--linteau-ring: ${tokens.ring};
|
|
209
|
+
--linteau-radius: ${tokens.radius};
|
|
210
|
+
--linteau-radius-sm: ${tokens.radiusSm};
|
|
211
|
+
--linteau-radius-md: ${tokens.radiusMd};
|
|
212
|
+
--linteau-radius-lg: ${tokens.radiusLg};
|
|
213
|
+
--linteau-radius-xl: ${tokens.radiusXl};
|
|
214
|
+
}`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/core/theme.ts
|
|
218
|
+
var TOKENS_STYLE_ID = "linteau-tokens";
|
|
219
|
+
var currentTheme = { ...defaultTheme };
|
|
220
|
+
var emitter = createEmitter();
|
|
221
|
+
function applyThemeToDom() {
|
|
222
|
+
if (!isBrowser()) return;
|
|
223
|
+
let style = document.getElementById(TOKENS_STYLE_ID);
|
|
224
|
+
if (!style) {
|
|
225
|
+
style = document.createElement("style");
|
|
226
|
+
style.id = TOKENS_STYLE_ID;
|
|
227
|
+
document.head.appendChild(style);
|
|
228
|
+
}
|
|
229
|
+
style.textContent = buildTokensCss(currentTheme);
|
|
230
|
+
}
|
|
231
|
+
function getTheme() {
|
|
232
|
+
return currentTheme;
|
|
233
|
+
}
|
|
234
|
+
function setTheme(overrides) {
|
|
235
|
+
currentTheme = { ...currentTheme, ...overrides };
|
|
236
|
+
applyThemeToDom();
|
|
237
|
+
emitter.emit("change", currentTheme);
|
|
238
|
+
}
|
|
239
|
+
function resetTheme() {
|
|
240
|
+
currentTheme = { ...defaultTheme };
|
|
241
|
+
applyThemeToDom();
|
|
242
|
+
emitter.emit("change", currentTheme);
|
|
243
|
+
}
|
|
244
|
+
function onThemeChange(listener) {
|
|
245
|
+
return emitter.on("change", listener);
|
|
246
|
+
}
|
|
247
|
+
applyThemeToDom();
|
|
248
|
+
|
|
249
|
+
// src/svg/Icon.ts
|
|
250
|
+
var SVGNS = "http://www.w3.org/2000/svg";
|
|
251
|
+
function baseAttrs(props) {
|
|
252
|
+
const size = String(props.size ?? 16);
|
|
253
|
+
return {
|
|
254
|
+
xmlns: SVGNS,
|
|
255
|
+
width: size,
|
|
256
|
+
height: size,
|
|
257
|
+
viewBox: "0 0 24 24",
|
|
258
|
+
fill: "none",
|
|
259
|
+
stroke: props.color ?? "currentColor",
|
|
260
|
+
"stroke-width": String(props.strokeWidth ?? 2),
|
|
261
|
+
"stroke-linecap": "round",
|
|
262
|
+
"stroke-linejoin": "round"
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function renderIconShape(shape, props) {
|
|
266
|
+
const svg = document.createElementNS(SVGNS, "svg");
|
|
267
|
+
const attrs = baseAttrs(props);
|
|
268
|
+
for (const key in attrs) svg.setAttribute(key, attrs[key]);
|
|
269
|
+
if (props.className) svg.setAttribute("class", props.className);
|
|
270
|
+
for (const [tag, childAttrs] of shape) {
|
|
271
|
+
const node = document.createElementNS(SVGNS, tag);
|
|
272
|
+
for (const key in childAttrs) node.setAttribute(key, childAttrs[key]);
|
|
273
|
+
svg.appendChild(node);
|
|
274
|
+
}
|
|
275
|
+
return svg;
|
|
276
|
+
}
|
|
277
|
+
function fromElement(source, props) {
|
|
278
|
+
const svg = source.cloneNode(true);
|
|
279
|
+
const size = String(props.size ?? 16);
|
|
280
|
+
svg.setAttribute("width", size);
|
|
281
|
+
svg.setAttribute("height", size);
|
|
282
|
+
if (props.color) svg.setAttribute("stroke", props.color);
|
|
283
|
+
if (props.strokeWidth != null) svg.setAttribute("stroke-width", String(props.strokeWidth));
|
|
284
|
+
if (props.className) svg.classList.add(...props.className.split(" ").filter(Boolean));
|
|
285
|
+
return svg;
|
|
286
|
+
}
|
|
287
|
+
function fromMarkup(markup, props) {
|
|
288
|
+
const doc = new DOMParser().parseFromString(markup, "image/svg+xml");
|
|
289
|
+
return fromElement(doc.documentElement, props);
|
|
290
|
+
}
|
|
291
|
+
function Icon(input, size, props = {}) {
|
|
292
|
+
if (input === null || input === void 0) return null;
|
|
293
|
+
const resolved = { ...size !== void 0 ? { size } : {}, ...props };
|
|
294
|
+
if (typeof input === "function") return input(resolved);
|
|
295
|
+
if (typeof input === "string") return fromMarkup(input, resolved);
|
|
296
|
+
if (input instanceof SVGElement) return fromElement(input, resolved);
|
|
297
|
+
if (Array.isArray(input)) return renderIconShape(input, resolved);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/svg/create-icon.ts
|
|
302
|
+
function createIcon(name, shape) {
|
|
303
|
+
return function renderIcon(props = {}) {
|
|
304
|
+
const svg = renderIconShape(shape, props);
|
|
305
|
+
svg.setAttribute("data-icon", name);
|
|
306
|
+
return svg;
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// src/svg/icons/alertTriangle.ts
|
|
311
|
+
var alertTriangle = createIcon("alert-triangle", [
|
|
312
|
+
["path", { d: "M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" }],
|
|
313
|
+
["line", { x1: "12", y1: "9", x2: "12", y2: "13" }],
|
|
314
|
+
["line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }]
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
// src/svg/icons/bell.ts
|
|
318
|
+
var bell = createIcon("bell", [
|
|
319
|
+
["path", { d: "M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" }],
|
|
320
|
+
["path", { d: "M10.3 21a1.94 1.94 0 0 0 3.4 0" }]
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
// src/svg/icons/calendar.ts
|
|
324
|
+
var calendar = createIcon("calendar", [
|
|
325
|
+
["rect", { x: "3", y: "4", width: "18", height: "18", rx: "2" }],
|
|
326
|
+
["line", { x1: "16", y1: "2", x2: "16", y2: "6" }],
|
|
327
|
+
["line", { x1: "8", y1: "2", x2: "8", y2: "6" }],
|
|
328
|
+
["line", { x1: "3", y1: "10", x2: "21", y2: "10" }]
|
|
329
|
+
]);
|
|
330
|
+
|
|
331
|
+
// src/svg/icons/check.ts
|
|
332
|
+
var check = createIcon("check", [
|
|
333
|
+
["polyline", { points: "20 6 9 17 4 12" }]
|
|
334
|
+
]);
|
|
335
|
+
|
|
336
|
+
// src/svg/icons/chevronDown.ts
|
|
337
|
+
var chevronDown = createIcon("chevron-down", [
|
|
338
|
+
["polyline", { points: "6 9 12 15 18 9" }]
|
|
339
|
+
]);
|
|
340
|
+
|
|
341
|
+
// src/svg/icons/chevronLeft.ts
|
|
342
|
+
var chevronLeft = createIcon("chevron-left", [
|
|
343
|
+
["polyline", { points: "15 18 9 12 15 6" }]
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
// src/svg/icons/chevronRight.ts
|
|
347
|
+
var chevronRight = createIcon("chevron-right", [
|
|
348
|
+
["polyline", { points: "9 18 15 12 9 6" }]
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
// src/svg/icons/chevronUp.ts
|
|
352
|
+
var chevronUp = createIcon("chevron-up", [
|
|
353
|
+
["polyline", { points: "18 15 12 9 6 15" }]
|
|
354
|
+
]);
|
|
355
|
+
|
|
356
|
+
// src/svg/icons/circle.ts
|
|
357
|
+
var circle = createIcon("circle", [
|
|
358
|
+
["circle", { cx: "12", cy: "12", r: "10" }]
|
|
359
|
+
]);
|
|
360
|
+
|
|
361
|
+
// src/svg/icons/copy.ts
|
|
362
|
+
var copy = createIcon("copy", [
|
|
363
|
+
["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }],
|
|
364
|
+
["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
// src/svg/icons/download.ts
|
|
368
|
+
var download = createIcon("download", [
|
|
369
|
+
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
|
370
|
+
["polyline", { points: "7 10 12 15 17 10" }],
|
|
371
|
+
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
|
|
372
|
+
]);
|
|
373
|
+
|
|
374
|
+
// src/svg/icons/edit.ts
|
|
375
|
+
var edit = createIcon("edit", [
|
|
376
|
+
["path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }]
|
|
377
|
+
]);
|
|
378
|
+
|
|
379
|
+
// src/svg/icons/externalLink.ts
|
|
380
|
+
var externalLink = createIcon("external-link", [
|
|
381
|
+
["path", { d: "M15 3h6v6" }],
|
|
382
|
+
["path", { d: "M10 14 21 3" }],
|
|
383
|
+
["path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }]
|
|
384
|
+
]);
|
|
385
|
+
|
|
386
|
+
// src/svg/icons/eye.ts
|
|
387
|
+
var eye = createIcon("eye", [
|
|
388
|
+
["path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }],
|
|
389
|
+
["circle", { cx: "12", cy: "12", r: "3" }]
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
// src/svg/icons/eyeOff.ts
|
|
393
|
+
var eyeOff = createIcon("eye-off", [
|
|
394
|
+
["path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }],
|
|
395
|
+
["path", { d: "M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" }],
|
|
396
|
+
["path", { d: "M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" }],
|
|
397
|
+
["line", { x1: "2", y1: "2", x2: "22", y2: "22" }]
|
|
398
|
+
]);
|
|
399
|
+
|
|
400
|
+
// src/svg/icons/filter.ts
|
|
401
|
+
var filter = createIcon("filter", [
|
|
402
|
+
["polygon", { points: "22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" }]
|
|
403
|
+
]);
|
|
404
|
+
|
|
405
|
+
// src/svg/icons/heart.ts
|
|
406
|
+
var heart = createIcon("heart", [
|
|
407
|
+
["path", { d: "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z" }]
|
|
408
|
+
]);
|
|
409
|
+
|
|
410
|
+
// src/svg/icons/home.ts
|
|
411
|
+
var home = createIcon("home", [
|
|
412
|
+
["path", { d: "M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" }],
|
|
413
|
+
["polyline", { points: "9 22 9 12 15 12 15 22" }]
|
|
414
|
+
]);
|
|
415
|
+
|
|
416
|
+
// src/svg/icons/info.ts
|
|
417
|
+
var info = createIcon("info", [
|
|
418
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
419
|
+
["line", { x1: "12", y1: "16", x2: "12", y2: "12" }],
|
|
420
|
+
["line", { x1: "12", y1: "8", x2: "12.01", y2: "8" }]
|
|
421
|
+
]);
|
|
422
|
+
|
|
423
|
+
// src/svg/icons/link.ts
|
|
424
|
+
var link = createIcon("link", [
|
|
425
|
+
["path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }],
|
|
426
|
+
["path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }],
|
|
427
|
+
["line", { x1: "8", y1: "12", x2: "16", y2: "12" }]
|
|
428
|
+
]);
|
|
429
|
+
|
|
430
|
+
// src/svg/icons/loader.ts
|
|
431
|
+
var loader = createIcon("loader", [
|
|
432
|
+
["line", { x1: "12", y1: "2", x2: "12", y2: "6" }],
|
|
433
|
+
["line", { x1: "12", y1: "18", x2: "12", y2: "22" }],
|
|
434
|
+
["line", { x1: "4.9", y1: "4.9", x2: "7.8", y2: "7.8" }],
|
|
435
|
+
["line", { x1: "16.2", y1: "16.2", x2: "19.1", y2: "19.1" }],
|
|
436
|
+
["line", { x1: "2", y1: "12", x2: "6", y2: "12" }],
|
|
437
|
+
["line", { x1: "18", y1: "12", x2: "22", y2: "12" }],
|
|
438
|
+
["line", { x1: "4.9", y1: "19.1", x2: "7.8", y2: "16.2" }],
|
|
439
|
+
["line", { x1: "16.2", y1: "7.8", x2: "19.1", y2: "4.9" }]
|
|
440
|
+
]);
|
|
441
|
+
|
|
442
|
+
// src/svg/icons/lock.ts
|
|
443
|
+
var lock = createIcon("lock", [
|
|
444
|
+
["rect", { x: "3", y: "11", width: "18", height: "11", rx: "2", ry: "2" }],
|
|
445
|
+
["path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }]
|
|
446
|
+
]);
|
|
447
|
+
|
|
448
|
+
// src/svg/icons/mail.ts
|
|
449
|
+
var mail = createIcon("mail", [
|
|
450
|
+
["rect", { width: "20", height: "16", x: "2", y: "4", rx: "2" }],
|
|
451
|
+
["path", { d: "m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" }]
|
|
452
|
+
]);
|
|
453
|
+
|
|
454
|
+
// src/svg/icons/menu.ts
|
|
455
|
+
var menu = createIcon("menu", [
|
|
456
|
+
["line", { x1: "4", y1: "12", x2: "20", y2: "12" }],
|
|
457
|
+
["line", { x1: "4", y1: "6", x2: "20", y2: "6" }],
|
|
458
|
+
["line", { x1: "4", y1: "18", x2: "20", y2: "18" }]
|
|
459
|
+
]);
|
|
460
|
+
|
|
461
|
+
// src/svg/icons/minus.ts
|
|
462
|
+
var minus = createIcon("minus", [
|
|
463
|
+
["line", { x1: "5", y1: "12", x2: "19", y2: "12" }]
|
|
464
|
+
]);
|
|
465
|
+
|
|
466
|
+
// src/svg/icons/moon.ts
|
|
467
|
+
var moon = createIcon("moon", [
|
|
468
|
+
["path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" }]
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
// src/svg/icons/moreHorizontal.ts
|
|
472
|
+
var moreHorizontal = createIcon("more-horizontal", [
|
|
473
|
+
["circle", { cx: "5", cy: "12", r: "1" }],
|
|
474
|
+
["circle", { cx: "12", cy: "12", r: "1" }],
|
|
475
|
+
["circle", { cx: "19", cy: "12", r: "1" }]
|
|
476
|
+
]);
|
|
477
|
+
|
|
478
|
+
// src/svg/icons/phone.ts
|
|
479
|
+
var phone = createIcon("phone", [
|
|
480
|
+
["path", { d: "M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z" }]
|
|
481
|
+
]);
|
|
482
|
+
|
|
483
|
+
// src/svg/icons/plus.ts
|
|
484
|
+
var plus = createIcon("plus", [
|
|
485
|
+
["line", { x1: "12", y1: "5", x2: "12", y2: "19" }],
|
|
486
|
+
["line", { x1: "5", y1: "12", x2: "19", y2: "12" }]
|
|
487
|
+
]);
|
|
488
|
+
|
|
489
|
+
// src/svg/icons/refreshCw.ts
|
|
490
|
+
var refreshCw = createIcon("refresh-cw", [
|
|
491
|
+
["path", { d: "M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }],
|
|
492
|
+
["path", { d: "M3 3v5h5" }],
|
|
493
|
+
["path", { d: "M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" }],
|
|
494
|
+
["path", { d: "M16 16h5v5" }]
|
|
495
|
+
]);
|
|
496
|
+
|
|
497
|
+
// src/svg/icons/search.ts
|
|
498
|
+
var search = createIcon("search", [
|
|
499
|
+
["circle", { cx: "11", cy: "11", r: "8" }],
|
|
500
|
+
["line", { x1: "21", y1: "21", x2: "16.65", y2: "16.65" }]
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
// src/svg/icons/settings.ts
|
|
504
|
+
var settings = createIcon("settings", [
|
|
505
|
+
["path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }],
|
|
506
|
+
["circle", { cx: "12", cy: "12", r: "3" }]
|
|
507
|
+
]);
|
|
508
|
+
|
|
509
|
+
// src/svg/icons/star.ts
|
|
510
|
+
var star = createIcon("star", [
|
|
511
|
+
["polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" }]
|
|
512
|
+
]);
|
|
513
|
+
|
|
514
|
+
// src/svg/icons/sun.ts
|
|
515
|
+
var sun = createIcon("sun", [
|
|
516
|
+
["circle", { cx: "12", cy: "12", r: "4" }],
|
|
517
|
+
["line", { x1: "12", y1: "2", x2: "12", y2: "4" }],
|
|
518
|
+
["line", { x1: "12", y1: "20", x2: "12", y2: "22" }],
|
|
519
|
+
["line", { x1: "4.93", y1: "4.93", x2: "6.34", y2: "6.34" }],
|
|
520
|
+
["line", { x1: "17.66", y1: "17.66", x2: "19.07", y2: "19.07" }],
|
|
521
|
+
["line", { x1: "2", y1: "12", x2: "4", y2: "12" }],
|
|
522
|
+
["line", { x1: "20", y1: "12", x2: "22", y2: "12" }],
|
|
523
|
+
["line", { x1: "6.34", y1: "17.66", x2: "4.93", y2: "19.07" }],
|
|
524
|
+
["line", { x1: "19.07", y1: "4.93", x2: "17.66", y2: "6.34" }]
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
// src/svg/icons/trash.ts
|
|
528
|
+
var trash = createIcon("trash", [
|
|
529
|
+
["path", { d: "M3 6h18" }],
|
|
530
|
+
["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }],
|
|
531
|
+
["path", { d: "M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" }],
|
|
532
|
+
["line", { x1: "10", y1: "11", x2: "10", y2: "17" }],
|
|
533
|
+
["line", { x1: "14", y1: "11", x2: "14", y2: "17" }]
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
// src/svg/icons/upload.ts
|
|
537
|
+
var upload = createIcon("upload", [
|
|
538
|
+
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
|
539
|
+
["polyline", { points: "17 8 12 3 7 8" }],
|
|
540
|
+
["line", { x1: "12", y1: "3", x2: "12", y2: "15" }]
|
|
541
|
+
]);
|
|
542
|
+
|
|
543
|
+
// src/svg/icons/user.ts
|
|
544
|
+
var user = createIcon("user", [
|
|
545
|
+
["path", { d: "M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2" }],
|
|
546
|
+
["circle", { cx: "12", cy: "7", r: "4" }]
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
// src/svg/icons/x.ts
|
|
550
|
+
var x = createIcon("x", [
|
|
551
|
+
["line", { x1: "18", y1: "6", x2: "6", y2: "18" }],
|
|
552
|
+
["line", { x1: "6", y1: "6", x2: "18", y2: "18" }]
|
|
553
|
+
]);
|
|
554
|
+
|
|
555
|
+
// src/svg/icons/index.ts
|
|
556
|
+
var icons = {
|
|
557
|
+
alertTriangle,
|
|
558
|
+
bell,
|
|
559
|
+
calendar,
|
|
560
|
+
check,
|
|
561
|
+
chevronDown,
|
|
562
|
+
chevronLeft,
|
|
563
|
+
chevronRight,
|
|
564
|
+
chevronUp,
|
|
565
|
+
circle,
|
|
566
|
+
copy,
|
|
567
|
+
download,
|
|
568
|
+
edit,
|
|
569
|
+
externalLink,
|
|
570
|
+
eye,
|
|
571
|
+
eyeOff,
|
|
572
|
+
filter,
|
|
573
|
+
heart,
|
|
574
|
+
home,
|
|
575
|
+
info,
|
|
576
|
+
link,
|
|
577
|
+
loader,
|
|
578
|
+
lock,
|
|
579
|
+
mail,
|
|
580
|
+
menu,
|
|
581
|
+
minus,
|
|
582
|
+
moon,
|
|
583
|
+
moreHorizontal,
|
|
584
|
+
phone,
|
|
585
|
+
plus,
|
|
586
|
+
refreshCw,
|
|
587
|
+
search,
|
|
588
|
+
settings,
|
|
589
|
+
star,
|
|
590
|
+
sun,
|
|
591
|
+
trash,
|
|
592
|
+
upload,
|
|
593
|
+
user,
|
|
594
|
+
x
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
// src/core/classNames.ts
|
|
598
|
+
var PREFIX = "linteau-";
|
|
599
|
+
function px(part) {
|
|
600
|
+
return `${PREFIX}${part}`;
|
|
601
|
+
}
|
|
602
|
+
function cx(...parts) {
|
|
603
|
+
return parts.filter(Boolean).join(" ");
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/core/styleInjector.ts
|
|
607
|
+
var STYLE_ID = "linteau-styles";
|
|
608
|
+
var injectedKeys = /* @__PURE__ */ new Set();
|
|
609
|
+
var baseCss = `
|
|
610
|
+
.linteau-hidden { display: none !important }
|
|
611
|
+
.linteau-icon { color: var(--linteau-muted-foreground); flex-shrink: 0 }
|
|
612
|
+
`;
|
|
613
|
+
function getStyleSheet() {
|
|
614
|
+
if (!isBrowser) return null;
|
|
615
|
+
let sheet = document.getElementById(STYLE_ID);
|
|
616
|
+
if (!sheet) {
|
|
617
|
+
sheet = document.createElement("style");
|
|
618
|
+
sheet.id = STYLE_ID;
|
|
619
|
+
document.head.appendChild(sheet);
|
|
620
|
+
}
|
|
621
|
+
return sheet;
|
|
622
|
+
}
|
|
623
|
+
function ensureComponentStyles(key, css) {
|
|
624
|
+
const sheet = getStyleSheet();
|
|
625
|
+
if (!sheet) return;
|
|
626
|
+
if (!injectedKeys.has("base")) {
|
|
627
|
+
sheet.appendChild(document.createTextNode(baseCss));
|
|
628
|
+
injectedKeys.add("base");
|
|
629
|
+
}
|
|
630
|
+
if (injectedKeys.has(key)) return;
|
|
631
|
+
sheet.appendChild(document.createTextNode(css));
|
|
632
|
+
injectedKeys.add(key);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/components/avatar/Avatar.ts
|
|
636
|
+
var avatarCss = `
|
|
637
|
+
.linteau-avatar { display: inline-flex; align-items: center; justify-content: center; background: var(--linteau-secondary); color: var(--linteau-secondary-foreground); font-size: 12px; font-weight: 600; overflow: hidden; flex-shrink: 0 }
|
|
638
|
+
.linteau-avatar-circle { border-radius: 50% }
|
|
639
|
+
.linteau-avatar-square { border-radius: var(--linteau-radius-sm) }
|
|
640
|
+
.linteau-avatar img { width: 100%; height: 100%; object-fit: cover }
|
|
641
|
+
`;
|
|
642
|
+
function Avatar(options = {}) {
|
|
643
|
+
assertDom("Avatar");
|
|
644
|
+
ensureComponentStyles("avatar", avatarCss);
|
|
645
|
+
const size = options.size ?? 36;
|
|
646
|
+
const shape = options.shape ?? "circle";
|
|
647
|
+
const root = el("div", cx(px("avatar"), px(`avatar-${shape}`)));
|
|
648
|
+
root.style.width = `${size}px`;
|
|
649
|
+
root.style.height = `${size}px`;
|
|
650
|
+
function render(src) {
|
|
651
|
+
root.replaceChildren();
|
|
652
|
+
if (src) {
|
|
653
|
+
const img = document.createElement("img");
|
|
654
|
+
img.src = src;
|
|
655
|
+
img.alt = options.alt ?? "";
|
|
656
|
+
img.addEventListener("error", () => render(void 0), { once: true });
|
|
657
|
+
root.appendChild(img);
|
|
658
|
+
} else {
|
|
659
|
+
const fallback = document.createElement("span");
|
|
660
|
+
fallback.textContent = options.initials ?? "";
|
|
661
|
+
root.appendChild(fallback);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
render(options.src);
|
|
665
|
+
const api = {
|
|
666
|
+
setSrc(src) {
|
|
667
|
+
render(src);
|
|
668
|
+
},
|
|
669
|
+
destroy() {
|
|
670
|
+
root.remove();
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
return attachController(root, api);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/components/badge/Badge.ts
|
|
677
|
+
var badgeCss = `
|
|
678
|
+
.linteau-badge {
|
|
679
|
+
display: inline-flex;
|
|
680
|
+
align-items: center;
|
|
681
|
+
gap: 4px;
|
|
682
|
+
padding: 2px 8px;
|
|
683
|
+
border-radius: 999px;
|
|
684
|
+
font-size: 11px;
|
|
685
|
+
font-weight: 600;
|
|
686
|
+
line-height: 1.6;
|
|
687
|
+
border: 1px solid transparent;
|
|
688
|
+
}
|
|
689
|
+
.linteau-badge-default { background: var(--linteau-secondary); color: var(--linteau-secondary-foreground) }
|
|
690
|
+
.linteau-badge-outline { background: transparent; border-color: var(--linteau-border); color: var(--linteau-foreground) }
|
|
691
|
+
.linteau-badge-success { background: var(--linteau-success); color: var(--linteau-success-foreground) }
|
|
692
|
+
.linteau-badge-warning { background: var(--linteau-warning); color: var(--linteau-warning-foreground) }
|
|
693
|
+
.linteau-badge-destructive { background: var(--linteau-destructive); color: var(--linteau-destructive-foreground) }
|
|
694
|
+
`;
|
|
695
|
+
function Badge(text, options = {}) {
|
|
696
|
+
assertDom("Badge");
|
|
697
|
+
ensureComponentStyles("badge", badgeCss);
|
|
698
|
+
let variant = options.variant ?? "default";
|
|
699
|
+
const span = el("span", cx(px("badge"), px(`badge-${variant}`)));
|
|
700
|
+
const label = document.createElement("span");
|
|
701
|
+
label.textContent = text;
|
|
702
|
+
if (options.icon) span.appendChild(Icon(options.icon, 11, { className: px("icon") }));
|
|
703
|
+
span.appendChild(label);
|
|
704
|
+
const api = {
|
|
705
|
+
setLabel(newText) {
|
|
706
|
+
label.textContent = newText;
|
|
707
|
+
},
|
|
708
|
+
setVariant(newVariant) {
|
|
709
|
+
span.classList.remove(px(`badge-${variant}`));
|
|
710
|
+
variant = newVariant;
|
|
711
|
+
span.classList.add(px(`badge-${variant}`));
|
|
712
|
+
},
|
|
713
|
+
destroy() {
|
|
714
|
+
span.remove();
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
return attachController(span, api);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/components/button/Button.ts
|
|
721
|
+
var buttonCss = `
|
|
722
|
+
.linteau-button {
|
|
723
|
+
display: inline-flex;
|
|
724
|
+
align-items: center;
|
|
725
|
+
justify-content: center;
|
|
726
|
+
gap: 6px;
|
|
727
|
+
border-radius: var(--linteau-radius-sm);
|
|
728
|
+
border: 1px solid transparent;
|
|
729
|
+
font-size: 13px;
|
|
730
|
+
font-weight: 500;
|
|
731
|
+
cursor: pointer;
|
|
732
|
+
font-family: inherit;
|
|
733
|
+
transition: background 120ms ease, border-color 120ms ease, transform 120ms ease, opacity 120ms ease;
|
|
734
|
+
}
|
|
735
|
+
.linteau-button-default { padding: 10px 14px }
|
|
736
|
+
.linteau-button-sm { padding: 6px 10px; font-size: 12px }
|
|
737
|
+
.linteau-button-full { width: 100% }
|
|
738
|
+
.linteau-button:active { transform: scale(0.97) }
|
|
739
|
+
.linteau-button:disabled { opacity: 0.5; cursor: not-allowed; transform: none }
|
|
740
|
+
.linteau-button-default:not(.linteau-button-outline):not(.linteau-button-ghost):not(.linteau-button-destructive) {
|
|
741
|
+
background: var(--linteau-primary);
|
|
742
|
+
color: var(--linteau-primary-foreground);
|
|
743
|
+
}
|
|
744
|
+
.linteau-button-default:not(.linteau-button-outline):not(.linteau-button-ghost):not(.linteau-button-destructive):hover:not(:disabled) {
|
|
745
|
+
filter: brightness(0.92);
|
|
746
|
+
}
|
|
747
|
+
.linteau-button-outline { background: transparent; border-color: var(--linteau-border); color: var(--linteau-foreground) }
|
|
748
|
+
.linteau-button-outline:hover:not(:disabled) { background: var(--linteau-accent) }
|
|
749
|
+
.linteau-button-ghost { background: transparent; color: var(--linteau-muted-foreground) }
|
|
750
|
+
.linteau-button-ghost:hover:not(:disabled) { background: var(--linteau-accent); color: var(--linteau-foreground) }
|
|
751
|
+
.linteau-button-destructive { background: var(--linteau-destructive); color: var(--linteau-destructive-foreground) }
|
|
752
|
+
.linteau-button-destructive:hover:not(:disabled) { filter: brightness(0.92) }
|
|
753
|
+
`;
|
|
754
|
+
function Button(text, options = {}) {
|
|
755
|
+
assertDom("Button");
|
|
756
|
+
ensureComponentStyles("button", buttonCss);
|
|
757
|
+
let { variant = "default", size = "default" } = options;
|
|
758
|
+
let { icon, iconPosition = "start", disabled = false } = options;
|
|
759
|
+
let onClickHandler = options.onClick;
|
|
760
|
+
const button = el("button", cx(px("button"), px(`button-${variant}`), px(`button-${size}`), options.fullWidth && px("button-full")));
|
|
761
|
+
button.type = "button";
|
|
762
|
+
button.disabled = disabled;
|
|
763
|
+
const label = document.createElement("span");
|
|
764
|
+
label.className = px("button-label");
|
|
765
|
+
label.textContent = text;
|
|
766
|
+
function render() {
|
|
767
|
+
button.replaceChildren();
|
|
768
|
+
const iconEl = icon ? Icon(icon, 14, { className: px("icon") }) : null;
|
|
769
|
+
const nodes = [];
|
|
770
|
+
if (iconEl && iconPosition === "start") nodes.push(iconEl);
|
|
771
|
+
nodes.push(label);
|
|
772
|
+
if (iconEl && iconPosition === "end") nodes.push(iconEl);
|
|
773
|
+
button.append(...nodes);
|
|
774
|
+
}
|
|
775
|
+
render();
|
|
776
|
+
const listener = (event) => {
|
|
777
|
+
if (!disabled) onClickHandler?.(event);
|
|
778
|
+
};
|
|
779
|
+
button.addEventListener("click", listener);
|
|
780
|
+
const api = {
|
|
781
|
+
setLabel(newText) {
|
|
782
|
+
label.textContent = newText;
|
|
783
|
+
},
|
|
784
|
+
setVariant(newVariant) {
|
|
785
|
+
button.classList.remove(px(`button-${variant}`));
|
|
786
|
+
variant = newVariant;
|
|
787
|
+
button.classList.add(px(`button-${variant}`));
|
|
788
|
+
},
|
|
789
|
+
setSize(newSize) {
|
|
790
|
+
button.classList.remove(px(`button-${size}`));
|
|
791
|
+
size = newSize;
|
|
792
|
+
button.classList.add(px(`button-${size}`));
|
|
793
|
+
},
|
|
794
|
+
setIcon(newIcon, position) {
|
|
795
|
+
icon = newIcon;
|
|
796
|
+
if (position) iconPosition = position;
|
|
797
|
+
render();
|
|
798
|
+
},
|
|
799
|
+
setDisabled(value) {
|
|
800
|
+
disabled = value;
|
|
801
|
+
button.disabled = value;
|
|
802
|
+
},
|
|
803
|
+
setOnClick(handler) {
|
|
804
|
+
onClickHandler = handler;
|
|
805
|
+
},
|
|
806
|
+
destroy() {
|
|
807
|
+
button.removeEventListener("click", listener);
|
|
808
|
+
button.remove();
|
|
809
|
+
}
|
|
810
|
+
};
|
|
811
|
+
return attachController(button, api);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/components/button/ButtonGroup.ts
|
|
815
|
+
var buttonGroupCss = `
|
|
816
|
+
.linteau-button-group { display: inline-flex; border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-sm); overflow: hidden }
|
|
817
|
+
.linteau-button-group .linteau-button { border-radius: 0; border: none; border-right: 1px solid var(--linteau-border); background: transparent; color: var(--linteau-muted-foreground) }
|
|
818
|
+
.linteau-button-group .linteau-button:last-child { border-right: none }
|
|
819
|
+
.linteau-button-group .linteau-button:hover:not(:disabled) { background: var(--linteau-accent) }
|
|
820
|
+
.linteau-button-group .linteau-button-group-item-active { background: var(--linteau-primary); color: var(--linteau-primary-foreground) }
|
|
821
|
+
`;
|
|
822
|
+
function ButtonGroup(options) {
|
|
823
|
+
assertDom("ButtonGroup");
|
|
824
|
+
ensureComponentStyles("button-group", buttonGroupCss);
|
|
825
|
+
let items = options.items;
|
|
826
|
+
let value = options.value;
|
|
827
|
+
const onChange = options.onChange;
|
|
828
|
+
const root = el("div", px("button-group"));
|
|
829
|
+
root.setAttribute("role", "group");
|
|
830
|
+
function render() {
|
|
831
|
+
root.replaceChildren();
|
|
832
|
+
for (const item of items) {
|
|
833
|
+
const isActive = item.value === value;
|
|
834
|
+
const button = Button(item.label, {
|
|
835
|
+
icon: item.icon,
|
|
836
|
+
variant: "ghost",
|
|
837
|
+
onClick: () => {
|
|
838
|
+
value = item.value;
|
|
839
|
+
render();
|
|
840
|
+
onChange?.(item.value);
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
button.classList.toggle(px("button-group-item-active"), isActive);
|
|
844
|
+
root.appendChild(button);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
render();
|
|
848
|
+
const api = {
|
|
849
|
+
getValue() {
|
|
850
|
+
return value;
|
|
851
|
+
},
|
|
852
|
+
setValue(newValue) {
|
|
853
|
+
value = newValue;
|
|
854
|
+
render();
|
|
855
|
+
},
|
|
856
|
+
setItems(newItems) {
|
|
857
|
+
items = newItems;
|
|
858
|
+
render();
|
|
859
|
+
},
|
|
860
|
+
destroy() {
|
|
861
|
+
root.remove();
|
|
862
|
+
}
|
|
863
|
+
};
|
|
864
|
+
return attachController(root, api);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// src/components/checkbox/Checkbox.ts
|
|
868
|
+
var checkboxCss = `
|
|
869
|
+
.linteau-checkbox-row { display: flex; align-items: center; gap: 8px }
|
|
870
|
+
.linteau-checkbox {
|
|
871
|
+
width: 18px;
|
|
872
|
+
height: 18px;
|
|
873
|
+
border-radius: var(--linteau-radius-sm);
|
|
874
|
+
border: 1px solid var(--linteau-border);
|
|
875
|
+
background: var(--linteau-secondary);
|
|
876
|
+
display: flex;
|
|
877
|
+
align-items: center;
|
|
878
|
+
justify-content: center;
|
|
879
|
+
cursor: pointer;
|
|
880
|
+
color: var(--linteau-primary-foreground);
|
|
881
|
+
}
|
|
882
|
+
.linteau-checkbox:disabled { opacity: 0.5; cursor: not-allowed }
|
|
883
|
+
.linteau-checkbox.linteau-checked { background: var(--linteau-primary); border-color: var(--linteau-primary) }
|
|
884
|
+
.linteau-checkbox svg { display: none }
|
|
885
|
+
.linteau-checkbox.linteau-checked svg, .linteau-checkbox.linteau-indeterminate svg { display: block }
|
|
886
|
+
`;
|
|
887
|
+
function Checkbox(options) {
|
|
888
|
+
assertDom("Checkbox");
|
|
889
|
+
ensureComponentStyles("checkbox", checkboxCss);
|
|
890
|
+
let checked = options.checked ?? options.defaultChecked ?? false;
|
|
891
|
+
let indeterminate = options.indeterminate ?? false;
|
|
892
|
+
let disabled = options.disabled ?? false;
|
|
893
|
+
const onChange = options.onChange;
|
|
894
|
+
const root = el("div", px("checkbox-row"));
|
|
895
|
+
const box = el("button", px("checkbox"));
|
|
896
|
+
box.type = "button";
|
|
897
|
+
box.setAttribute("role", "checkbox");
|
|
898
|
+
box.disabled = disabled;
|
|
899
|
+
const checkIcon = Icon(icons.check, 12);
|
|
900
|
+
const dashIcon = Icon(icons.minus, 12);
|
|
901
|
+
box.append(checkIcon, dashIcon);
|
|
902
|
+
const labelText = document.createElement("span");
|
|
903
|
+
labelText.textContent = options.label;
|
|
904
|
+
function render() {
|
|
905
|
+
box.classList.toggle(px("checked"), checked);
|
|
906
|
+
box.classList.toggle(px("indeterminate"), indeterminate && !checked);
|
|
907
|
+
box.setAttribute("aria-checked", indeterminate ? "mixed" : String(checked));
|
|
908
|
+
checkIcon.style.display = checked ? "block" : "none";
|
|
909
|
+
dashIcon.style.display = !checked && indeterminate ? "block" : "none";
|
|
910
|
+
}
|
|
911
|
+
render();
|
|
912
|
+
const listener = () => {
|
|
913
|
+
if (disabled) return;
|
|
914
|
+
checked = !checked;
|
|
915
|
+
indeterminate = false;
|
|
916
|
+
render();
|
|
917
|
+
onChange?.(checked);
|
|
918
|
+
};
|
|
919
|
+
box.addEventListener("click", listener);
|
|
920
|
+
root.append(box, labelText);
|
|
921
|
+
const api = {
|
|
922
|
+
getValue() {
|
|
923
|
+
return checked;
|
|
924
|
+
},
|
|
925
|
+
setValue(value) {
|
|
926
|
+
checked = value;
|
|
927
|
+
indeterminate = false;
|
|
928
|
+
render();
|
|
929
|
+
},
|
|
930
|
+
setIndeterminate(value) {
|
|
931
|
+
indeterminate = value;
|
|
932
|
+
render();
|
|
933
|
+
},
|
|
934
|
+
setDisabled(value) {
|
|
935
|
+
disabled = value;
|
|
936
|
+
box.disabled = value;
|
|
937
|
+
},
|
|
938
|
+
destroy() {
|
|
939
|
+
box.removeEventListener("click", listener);
|
|
940
|
+
root.remove();
|
|
941
|
+
}
|
|
942
|
+
};
|
|
943
|
+
return attachController(root, api);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// src/core/overlay.ts
|
|
947
|
+
function onClickOutside(targets, callback) {
|
|
948
|
+
if (!isBrowser()) return () => {
|
|
949
|
+
};
|
|
950
|
+
const list = Array.isArray(targets) ? targets : [targets];
|
|
951
|
+
const listener = (event) => {
|
|
952
|
+
const node = event.target;
|
|
953
|
+
if (list.some((target) => target.contains(node))) return;
|
|
954
|
+
callback();
|
|
955
|
+
};
|
|
956
|
+
document.addEventListener("mousedown", listener, true);
|
|
957
|
+
return () => document.removeEventListener("mousedown", listener, true);
|
|
958
|
+
}
|
|
959
|
+
function onEscapeKey(callback) {
|
|
960
|
+
if (!isBrowser()) return () => {
|
|
961
|
+
};
|
|
962
|
+
const listener = (event) => {
|
|
963
|
+
if (event.key === "Escape") callback();
|
|
964
|
+
};
|
|
965
|
+
document.addEventListener("keydown", listener);
|
|
966
|
+
return () => document.removeEventListener("keydown", listener);
|
|
967
|
+
}
|
|
968
|
+
function lockBodyScroll() {
|
|
969
|
+
if (!isBrowser()) return () => {
|
|
970
|
+
};
|
|
971
|
+
const previous = document.body.style.overflow;
|
|
972
|
+
document.body.style.overflow = "hidden";
|
|
973
|
+
return () => {
|
|
974
|
+
document.body.style.overflow = previous;
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
function positionFloating(anchor, floating, placement = "bottom", gap = 6) {
|
|
978
|
+
if (!isBrowser()) return;
|
|
979
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
980
|
+
const floatingRect = floating.getBoundingClientRect();
|
|
981
|
+
let top = 0;
|
|
982
|
+
let left = 0;
|
|
983
|
+
if (placement === "bottom") {
|
|
984
|
+
top = anchorRect.bottom + gap;
|
|
985
|
+
left = anchorRect.left;
|
|
986
|
+
} else if (placement === "top") {
|
|
987
|
+
top = anchorRect.top - floatingRect.height - gap;
|
|
988
|
+
left = anchorRect.left;
|
|
989
|
+
} else if (placement === "left") {
|
|
990
|
+
top = anchorRect.top;
|
|
991
|
+
left = anchorRect.left - floatingRect.width - gap;
|
|
992
|
+
} else {
|
|
993
|
+
top = anchorRect.top;
|
|
994
|
+
left = anchorRect.right + gap;
|
|
995
|
+
}
|
|
996
|
+
const maxLeft = window.innerWidth - floatingRect.width - 8;
|
|
997
|
+
const maxTop = window.innerHeight - floatingRect.height - 8;
|
|
998
|
+
floating.style.position = "fixed";
|
|
999
|
+
floating.style.top = `${Math.max(8, Math.min(top, maxTop))}px`;
|
|
1000
|
+
floating.style.left = `${Math.max(8, Math.min(left, maxLeft))}px`;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/components/command/Command.ts
|
|
1004
|
+
var commandCss = `
|
|
1005
|
+
.linteau-command-overlay { position: fixed; inset: 0; background: color-mix(in oklch, black 60%, transparent); display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh; z-index: 1500; backdrop-filter: blur(4px) }
|
|
1006
|
+
.linteau-command-panel { width: min(560px, 92vw); max-height: 60vh; display: flex; flex-direction: column; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); overflow: hidden }
|
|
1007
|
+
.linteau-command-input-wrap { display: flex; align-items: center; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--linteau-border) }
|
|
1008
|
+
.linteau-command-input { flex: 1; background: transparent; border: none; outline: none; color: var(--linteau-foreground); font-size: 14px; font-family: inherit }
|
|
1009
|
+
.linteau-command-list { overflow-y: auto; padding: 6px; flex: 1 }
|
|
1010
|
+
.linteau-command-group-heading { padding: 6px 8px 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--linteau-muted-foreground) }
|
|
1011
|
+
.linteau-command-item { display: flex; align-items: center; gap: 8px; padding: 8px; border-radius: var(--linteau-radius-sm); font-size: 13px; color: var(--linteau-foreground); cursor: pointer }
|
|
1012
|
+
.linteau-command-item-highlighted { background: var(--linteau-accent) }
|
|
1013
|
+
.linteau-command-item-disabled { opacity: 0.5; cursor: not-allowed }
|
|
1014
|
+
.linteau-command-empty { padding: 24px; text-align: center; font-size: 12px; color: var(--linteau-muted-foreground) }
|
|
1015
|
+
`;
|
|
1016
|
+
function Command(options = {}) {
|
|
1017
|
+
assertDom("Command");
|
|
1018
|
+
ensureComponentStyles("command", commandCss);
|
|
1019
|
+
let groups = options.groups ?? [];
|
|
1020
|
+
const closeOnSelect = options.closeOnSelect ?? true;
|
|
1021
|
+
const onSelect = options.onSelect;
|
|
1022
|
+
let query = "";
|
|
1023
|
+
let highlighted = 0;
|
|
1024
|
+
let isOpen = false;
|
|
1025
|
+
let unlockScroll = null;
|
|
1026
|
+
let unbindEscape = null;
|
|
1027
|
+
const overlay = el("div", cx(px("command-overlay"), px("hidden")));
|
|
1028
|
+
const panel = el("div", px("command-panel"));
|
|
1029
|
+
panel.setAttribute("role", "dialog");
|
|
1030
|
+
panel.setAttribute("aria-modal", "true");
|
|
1031
|
+
const inputWrap = el("div", px("command-input-wrap"));
|
|
1032
|
+
const searchIcon = Icon(icons.search, 14, { className: px("icon") });
|
|
1033
|
+
if (searchIcon) inputWrap.appendChild(searchIcon);
|
|
1034
|
+
const input = el("input", px("command-input"));
|
|
1035
|
+
input.type = "text";
|
|
1036
|
+
input.placeholder = options.placeholder ?? "Type a command or search...";
|
|
1037
|
+
inputWrap.appendChild(input);
|
|
1038
|
+
const list = el("div", px("command-list"));
|
|
1039
|
+
panel.append(inputWrap, list);
|
|
1040
|
+
overlay.appendChild(panel);
|
|
1041
|
+
function filteredFlat() {
|
|
1042
|
+
const normalized = query.trim().toLowerCase();
|
|
1043
|
+
const all = groups.flatMap((group) => group.items);
|
|
1044
|
+
if (!normalized) return all;
|
|
1045
|
+
return all.filter((item) => item.label.toLowerCase().includes(normalized) || item.keywords?.some((k) => k.toLowerCase().includes(normalized)));
|
|
1046
|
+
}
|
|
1047
|
+
function renderList() {
|
|
1048
|
+
list.replaceChildren();
|
|
1049
|
+
const normalized = query.trim().toLowerCase();
|
|
1050
|
+
const flat = filteredFlat();
|
|
1051
|
+
if (flat.length === 0) {
|
|
1052
|
+
const empty = el("div", px("command-empty"));
|
|
1053
|
+
empty.textContent = options.emptyText ?? "No results found.";
|
|
1054
|
+
list.appendChild(empty);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
let runningIndex = 0;
|
|
1058
|
+
for (const group of groups) {
|
|
1059
|
+
const matchingItems = group.items.filter((item) => !normalized || item.label.toLowerCase().includes(normalized) || item.keywords?.some((k) => k.toLowerCase().includes(normalized)));
|
|
1060
|
+
if (matchingItems.length === 0) continue;
|
|
1061
|
+
if (group.heading) {
|
|
1062
|
+
const heading = el("div", px("command-group-heading"));
|
|
1063
|
+
heading.textContent = group.heading;
|
|
1064
|
+
list.appendChild(heading);
|
|
1065
|
+
}
|
|
1066
|
+
for (const item of matchingItems) {
|
|
1067
|
+
const index = runningIndex++;
|
|
1068
|
+
const itemEl = el("div", cx(px("command-item"), index === highlighted && px("command-item-highlighted"), item.disabled && px("command-item-disabled")));
|
|
1069
|
+
if (item.icon) itemEl.appendChild(Icon(item.icon, 14, { className: px("icon") }));
|
|
1070
|
+
const label = document.createElement("span");
|
|
1071
|
+
label.textContent = item.label;
|
|
1072
|
+
itemEl.appendChild(label);
|
|
1073
|
+
itemEl.addEventListener("mousedown", (event) => {
|
|
1074
|
+
event.preventDefault();
|
|
1075
|
+
selectItem(item);
|
|
1076
|
+
});
|
|
1077
|
+
itemEl.addEventListener("mouseenter", () => {
|
|
1078
|
+
highlighted = index;
|
|
1079
|
+
renderList();
|
|
1080
|
+
});
|
|
1081
|
+
list.appendChild(itemEl);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
function selectItem(item) {
|
|
1086
|
+
if (item.disabled) return;
|
|
1087
|
+
onSelect?.(item.value);
|
|
1088
|
+
if (closeOnSelect) close();
|
|
1089
|
+
}
|
|
1090
|
+
function handleKeydown(event) {
|
|
1091
|
+
const flat = filteredFlat();
|
|
1092
|
+
if (event.key === "ArrowDown") {
|
|
1093
|
+
event.preventDefault();
|
|
1094
|
+
highlighted = Math.min(highlighted + 1, flat.length - 1);
|
|
1095
|
+
renderList();
|
|
1096
|
+
} else if (event.key === "ArrowUp") {
|
|
1097
|
+
event.preventDefault();
|
|
1098
|
+
highlighted = Math.max(highlighted - 1, 0);
|
|
1099
|
+
renderList();
|
|
1100
|
+
} else if (event.key === "Enter") {
|
|
1101
|
+
event.preventDefault();
|
|
1102
|
+
const item = flat[highlighted];
|
|
1103
|
+
if (item) selectItem(item);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const inputListener = () => {
|
|
1107
|
+
query = input.value;
|
|
1108
|
+
highlighted = 0;
|
|
1109
|
+
renderList();
|
|
1110
|
+
};
|
|
1111
|
+
input.addEventListener("input", inputListener);
|
|
1112
|
+
input.addEventListener("keydown", handleKeydown);
|
|
1113
|
+
const backdropListener = (event) => {
|
|
1114
|
+
if (event.target === overlay) close();
|
|
1115
|
+
};
|
|
1116
|
+
overlay.addEventListener("mousedown", backdropListener);
|
|
1117
|
+
function open() {
|
|
1118
|
+
if (isOpen) return;
|
|
1119
|
+
isOpen = true;
|
|
1120
|
+
query = "";
|
|
1121
|
+
highlighted = 0;
|
|
1122
|
+
input.value = "";
|
|
1123
|
+
overlay.classList.remove(px("hidden"));
|
|
1124
|
+
renderList();
|
|
1125
|
+
unlockScroll = lockBodyScroll();
|
|
1126
|
+
unbindEscape = onEscapeKey(close);
|
|
1127
|
+
input.focus();
|
|
1128
|
+
}
|
|
1129
|
+
function close() {
|
|
1130
|
+
if (!isOpen) return;
|
|
1131
|
+
isOpen = false;
|
|
1132
|
+
overlay.classList.add(px("hidden"));
|
|
1133
|
+
unlockScroll?.();
|
|
1134
|
+
unlockScroll = null;
|
|
1135
|
+
unbindEscape?.();
|
|
1136
|
+
unbindEscape = null;
|
|
1137
|
+
}
|
|
1138
|
+
function toggle() {
|
|
1139
|
+
if (isOpen) close();
|
|
1140
|
+
else open();
|
|
1141
|
+
}
|
|
1142
|
+
const api = {
|
|
1143
|
+
open,
|
|
1144
|
+
close,
|
|
1145
|
+
toggle,
|
|
1146
|
+
isOpen() {
|
|
1147
|
+
return isOpen;
|
|
1148
|
+
},
|
|
1149
|
+
setGroups(newGroups) {
|
|
1150
|
+
groups = newGroups;
|
|
1151
|
+
renderList();
|
|
1152
|
+
},
|
|
1153
|
+
destroy() {
|
|
1154
|
+
input.removeEventListener("input", inputListener);
|
|
1155
|
+
input.removeEventListener("keydown", handleKeydown);
|
|
1156
|
+
overlay.removeEventListener("mousedown", backdropListener);
|
|
1157
|
+
unlockScroll?.();
|
|
1158
|
+
unbindEscape?.();
|
|
1159
|
+
overlay.remove();
|
|
1160
|
+
}
|
|
1161
|
+
};
|
|
1162
|
+
return attachController(overlay, api);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/components/data/List.ts
|
|
1166
|
+
var listCss = `
|
|
1167
|
+
.linteau-list { display: flex; flex-direction: column; gap: 2px; width: 100% }
|
|
1168
|
+
.linteau-list-item { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: var(--linteau-radius-sm); cursor: pointer }
|
|
1169
|
+
.linteau-list-item:hover { background: var(--linteau-accent) }
|
|
1170
|
+
.linteau-list-item-selected { background: var(--linteau-accent) }
|
|
1171
|
+
.linteau-list-item-text { display: flex; flex-direction: column }
|
|
1172
|
+
.linteau-list-item-label { font-size: 13px; color: var(--linteau-foreground) }
|
|
1173
|
+
.linteau-list-item-description { font-size: 11px; color: var(--linteau-muted-foreground) }
|
|
1174
|
+
`;
|
|
1175
|
+
function List(options) {
|
|
1176
|
+
assertDom("List");
|
|
1177
|
+
ensureComponentStyles("list", listCss);
|
|
1178
|
+
let items = options.items;
|
|
1179
|
+
const selectable = options.selectable ?? true;
|
|
1180
|
+
let value = options.value;
|
|
1181
|
+
const onChange = options.onChange;
|
|
1182
|
+
const cleanupListeners = [];
|
|
1183
|
+
const root = el("div", px("list"));
|
|
1184
|
+
function render() {
|
|
1185
|
+
root.replaceChildren();
|
|
1186
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1187
|
+
for (const item of items) {
|
|
1188
|
+
const row = el("div", cx(px("list-item"), selectable && item.value === value && px("list-item-selected")));
|
|
1189
|
+
if (item.icon) row.appendChild(Icon(item.icon, 16, { className: px("icon") }));
|
|
1190
|
+
const textWrap = el("div", px("list-item-text"));
|
|
1191
|
+
const label = el("div", px("list-item-label"));
|
|
1192
|
+
label.textContent = item.label;
|
|
1193
|
+
textWrap.appendChild(label);
|
|
1194
|
+
if (item.description) {
|
|
1195
|
+
const description = el("div", px("list-item-description"));
|
|
1196
|
+
description.textContent = item.description;
|
|
1197
|
+
textWrap.appendChild(description);
|
|
1198
|
+
}
|
|
1199
|
+
row.appendChild(textWrap);
|
|
1200
|
+
if (selectable) {
|
|
1201
|
+
const listener = () => {
|
|
1202
|
+
value = item.value;
|
|
1203
|
+
render();
|
|
1204
|
+
onChange?.(item.value);
|
|
1205
|
+
};
|
|
1206
|
+
row.addEventListener("click", listener);
|
|
1207
|
+
cleanupListeners.push(() => row.removeEventListener("click", listener));
|
|
1208
|
+
}
|
|
1209
|
+
root.appendChild(row);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
render();
|
|
1213
|
+
const api = {
|
|
1214
|
+
getValue() {
|
|
1215
|
+
return value;
|
|
1216
|
+
},
|
|
1217
|
+
setValue(newValue) {
|
|
1218
|
+
value = newValue;
|
|
1219
|
+
render();
|
|
1220
|
+
},
|
|
1221
|
+
setItems(newItems) {
|
|
1222
|
+
items = newItems;
|
|
1223
|
+
render();
|
|
1224
|
+
},
|
|
1225
|
+
destroy() {
|
|
1226
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1227
|
+
root.remove();
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
return attachController(root, api);
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
// src/components/data/Table.ts
|
|
1234
|
+
var tableCss = `
|
|
1235
|
+
.linteau-table-wrap { width: 100%; overflow-x: auto; border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg) }
|
|
1236
|
+
.linteau-table { width: 100%; border-collapse: collapse; font-size: 12px }
|
|
1237
|
+
.linteau-table th { text-align: left; padding: 10px 12px; background: var(--linteau-muted); color: var(--linteau-muted-foreground); font-weight: 600; border-bottom: 1px solid var(--linteau-border); cursor: default; white-space: nowrap }
|
|
1238
|
+
.linteau-table th.linteau-table-sortable { cursor: pointer; user-select: none }
|
|
1239
|
+
.linteau-table td { padding: 10px 12px; border-bottom: 1px solid var(--linteau-border); color: var(--linteau-foreground) }
|
|
1240
|
+
.linteau-table tr:last-child td { border-bottom: none }
|
|
1241
|
+
.linteau-table-sort-icon { display: inline-block; margin-left: 4px; vertical-align: middle }
|
|
1242
|
+
`;
|
|
1243
|
+
function Table(options) {
|
|
1244
|
+
assertDom("Table");
|
|
1245
|
+
ensureComponentStyles("table", tableCss);
|
|
1246
|
+
let rows = options.rows;
|
|
1247
|
+
let sortKey = null;
|
|
1248
|
+
let sortDirection = "asc";
|
|
1249
|
+
const cleanupListeners = [];
|
|
1250
|
+
const wrap = el("div", px("table-wrap"));
|
|
1251
|
+
const table = el("table", px("table"));
|
|
1252
|
+
const thead = document.createElement("thead");
|
|
1253
|
+
const tbody = document.createElement("tbody");
|
|
1254
|
+
function renderHead() {
|
|
1255
|
+
thead.replaceChildren();
|
|
1256
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1257
|
+
const headRow = document.createElement("tr");
|
|
1258
|
+
for (const column of options.columns) {
|
|
1259
|
+
const th = document.createElement("th");
|
|
1260
|
+
th.className = cx(px("table-sortable-cell"), column.sortable && px("table-sortable"));
|
|
1261
|
+
th.textContent = column.header;
|
|
1262
|
+
if (column.sortable) {
|
|
1263
|
+
if (sortKey === column.key) {
|
|
1264
|
+
const sortIcon = Icon(sortDirection === "asc" ? icons.chevronUp : icons.chevronDown, 12, { className: px("table-sort-icon") });
|
|
1265
|
+
if (sortIcon) th.appendChild(sortIcon);
|
|
1266
|
+
}
|
|
1267
|
+
const listener = () => {
|
|
1268
|
+
sortDirection = sortKey === column.key && sortDirection === "asc" ? "desc" : "asc";
|
|
1269
|
+
sortKey = column.key;
|
|
1270
|
+
renderHead();
|
|
1271
|
+
options.onSortChange?.(column.key, sortDirection);
|
|
1272
|
+
};
|
|
1273
|
+
th.addEventListener("click", listener);
|
|
1274
|
+
cleanupListeners.push(() => th.removeEventListener("click", listener));
|
|
1275
|
+
}
|
|
1276
|
+
headRow.appendChild(th);
|
|
1277
|
+
}
|
|
1278
|
+
thead.appendChild(headRow);
|
|
1279
|
+
}
|
|
1280
|
+
function renderBody() {
|
|
1281
|
+
tbody.replaceChildren();
|
|
1282
|
+
for (const row of rows) {
|
|
1283
|
+
const tr = document.createElement("tr");
|
|
1284
|
+
for (const column of options.columns) {
|
|
1285
|
+
const td = document.createElement("td");
|
|
1286
|
+
const content = column.render ? column.render(row) : String(row[column.key] ?? "");
|
|
1287
|
+
if (content instanceof HTMLElement) td.appendChild(content);
|
|
1288
|
+
else td.textContent = content;
|
|
1289
|
+
tr.appendChild(td);
|
|
1290
|
+
}
|
|
1291
|
+
tbody.appendChild(tr);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
renderHead();
|
|
1295
|
+
renderBody();
|
|
1296
|
+
table.append(thead, tbody);
|
|
1297
|
+
wrap.appendChild(table);
|
|
1298
|
+
const api = {
|
|
1299
|
+
setRows(newRows) {
|
|
1300
|
+
rows = newRows;
|
|
1301
|
+
renderBody();
|
|
1302
|
+
},
|
|
1303
|
+
destroy() {
|
|
1304
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1305
|
+
wrap.remove();
|
|
1306
|
+
}
|
|
1307
|
+
};
|
|
1308
|
+
return attachController(wrap, api);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/components/data/Tree.ts
|
|
1312
|
+
var treeCss = `
|
|
1313
|
+
.linteau-tree { display: flex; flex-direction: column; gap: 2px }
|
|
1314
|
+
.linteau-tree-row { display: flex; align-items: center; gap: 6px; padding: 6px 8px; border-radius: var(--linteau-radius-sm); cursor: pointer; font-size: 12px }
|
|
1315
|
+
.linteau-tree-row:hover { background: var(--linteau-accent) }
|
|
1316
|
+
.linteau-tree-chevron { transition: transform 150ms ease }
|
|
1317
|
+
.linteau-tree-chevron.linteau-open { transform: rotate(90deg) }
|
|
1318
|
+
.linteau-tree-children { margin-left: 18px; display: flex; flex-direction: column; gap: 2px }
|
|
1319
|
+
.linteau-tree-children.linteau-collapsed { display: none }
|
|
1320
|
+
`;
|
|
1321
|
+
function Tree(options) {
|
|
1322
|
+
assertDom("Tree");
|
|
1323
|
+
ensureComponentStyles("tree", treeCss);
|
|
1324
|
+
const expanded = new Set(options.defaultExpanded ?? []);
|
|
1325
|
+
const cleanupListeners = [];
|
|
1326
|
+
const allValues = [];
|
|
1327
|
+
const root = el("div", px("tree"));
|
|
1328
|
+
function collectValues(nodes) {
|
|
1329
|
+
for (const node of nodes) {
|
|
1330
|
+
if (node.children?.length) {
|
|
1331
|
+
allValues.push(node.value);
|
|
1332
|
+
collectValues(node.children);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
collectValues(options.nodes);
|
|
1337
|
+
function renderNode(node, container) {
|
|
1338
|
+
const hasChildren = Boolean(node.children?.length);
|
|
1339
|
+
const isOpen = expanded.has(node.value);
|
|
1340
|
+
const row = el("div", px("tree-row"));
|
|
1341
|
+
if (hasChildren) {
|
|
1342
|
+
const chevron = Icon(icons.chevronRight, 12, { className: cx(px("tree-chevron"), isOpen && px("open")) });
|
|
1343
|
+
if (chevron) row.appendChild(chevron);
|
|
1344
|
+
}
|
|
1345
|
+
if (node.icon) row.appendChild(Icon(node.icon, 14, { className: px("icon") }));
|
|
1346
|
+
const label = document.createElement("span");
|
|
1347
|
+
label.textContent = node.label;
|
|
1348
|
+
row.appendChild(label);
|
|
1349
|
+
const childrenContainer = hasChildren ? el("div", cx(px("tree-children"), !isOpen && px("collapsed"))) : null;
|
|
1350
|
+
const listener = () => {
|
|
1351
|
+
if (hasChildren) {
|
|
1352
|
+
if (expanded.has(node.value)) expanded.delete(node.value);
|
|
1353
|
+
else expanded.add(node.value);
|
|
1354
|
+
render();
|
|
1355
|
+
}
|
|
1356
|
+
options.onSelect?.(node.value);
|
|
1357
|
+
};
|
|
1358
|
+
row.addEventListener("click", listener);
|
|
1359
|
+
cleanupListeners.push(() => row.removeEventListener("click", listener));
|
|
1360
|
+
container.appendChild(row);
|
|
1361
|
+
if (childrenContainer) {
|
|
1362
|
+
for (const child of node.children ?? []) renderNode(child, childrenContainer);
|
|
1363
|
+
container.appendChild(childrenContainer);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
function render() {
|
|
1367
|
+
root.replaceChildren();
|
|
1368
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1369
|
+
for (const node of options.nodes) renderNode(node, root);
|
|
1370
|
+
}
|
|
1371
|
+
render();
|
|
1372
|
+
const api = {
|
|
1373
|
+
expandAll() {
|
|
1374
|
+
allValues.forEach((value) => expanded.add(value));
|
|
1375
|
+
render();
|
|
1376
|
+
},
|
|
1377
|
+
collapseAll() {
|
|
1378
|
+
expanded.clear();
|
|
1379
|
+
render();
|
|
1380
|
+
},
|
|
1381
|
+
destroy() {
|
|
1382
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
1383
|
+
root.remove();
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
return attachController(root, api);
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// src/components/feedback/Alert.ts
|
|
1390
|
+
var alertCss = `
|
|
1391
|
+
.linteau-alert { display: flex; gap: 10px; padding: 12px; border-radius: var(--linteau-radius-lg); border: 1px solid var(--linteau-border); background: var(--linteau-muted) }
|
|
1392
|
+
.linteau-alert-success { border-color: var(--linteau-success) }
|
|
1393
|
+
.linteau-alert-warning { border-color: var(--linteau-warning) }
|
|
1394
|
+
.linteau-alert-destructive { border-color: var(--linteau-destructive) }
|
|
1395
|
+
.linteau-alert-body { flex: 1 }
|
|
1396
|
+
.linteau-alert-title { font-size: 13px; font-weight: 600; margin-bottom: 2px }
|
|
1397
|
+
.linteau-alert-description { font-size: 12px; color: var(--linteau-muted-foreground) }
|
|
1398
|
+
.linteau-alert-close { background: transparent; border: none; color: var(--linteau-muted-foreground); cursor: pointer; display: flex; align-self: flex-start }
|
|
1399
|
+
`;
|
|
1400
|
+
var defaultIconByVariant = {
|
|
1401
|
+
default: icons.info,
|
|
1402
|
+
success: icons.check,
|
|
1403
|
+
warning: icons.alertTriangle,
|
|
1404
|
+
destructive: icons.alertTriangle
|
|
1405
|
+
};
|
|
1406
|
+
function Alert(description, options = {}) {
|
|
1407
|
+
assertDom("Alert");
|
|
1408
|
+
ensureComponentStyles("alert", alertCss);
|
|
1409
|
+
const variant = options.variant ?? "default";
|
|
1410
|
+
const root = el("div", cx(px("alert"), px(`alert-${variant}`)));
|
|
1411
|
+
const iconEl = Icon(options.icon ?? defaultIconByVariant[variant], 18, { className: px("icon") });
|
|
1412
|
+
if (iconEl) root.appendChild(iconEl);
|
|
1413
|
+
const body = el("div", px("alert-body"));
|
|
1414
|
+
if (options.title) {
|
|
1415
|
+
const title = el("div", px("alert-title"));
|
|
1416
|
+
title.textContent = options.title;
|
|
1417
|
+
body.appendChild(title);
|
|
1418
|
+
}
|
|
1419
|
+
const descriptionEl = el("div", px("alert-description"));
|
|
1420
|
+
descriptionEl.textContent = description;
|
|
1421
|
+
body.appendChild(descriptionEl);
|
|
1422
|
+
root.appendChild(body);
|
|
1423
|
+
let closeListener = null;
|
|
1424
|
+
if (options.dismissible) {
|
|
1425
|
+
const closeButton = el("button", px("alert-close"));
|
|
1426
|
+
closeButton.type = "button";
|
|
1427
|
+
const closeIcon = Icon(icons.x, 14);
|
|
1428
|
+
if (closeIcon) closeButton.appendChild(closeIcon);
|
|
1429
|
+
closeListener = () => {
|
|
1430
|
+
options.onDismiss?.();
|
|
1431
|
+
root.remove();
|
|
1432
|
+
};
|
|
1433
|
+
closeButton.addEventListener("click", closeListener);
|
|
1434
|
+
root.appendChild(closeButton);
|
|
1435
|
+
}
|
|
1436
|
+
const api = {
|
|
1437
|
+
setDescription(description2) {
|
|
1438
|
+
descriptionEl.textContent = description2;
|
|
1439
|
+
},
|
|
1440
|
+
destroy() {
|
|
1441
|
+
root.remove();
|
|
1442
|
+
}
|
|
1443
|
+
};
|
|
1444
|
+
return attachController(root, api);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/components/feedback/ProgressBar.ts
|
|
1448
|
+
var progressBarCss = `
|
|
1449
|
+
.linteau-progress-overlay { position: fixed; inset: 0; background: color-mix(in oklch, black 55%, transparent); display: flex; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(4px) }
|
|
1450
|
+
.linteau-progress-card { width: 280px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); padding: 20px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5) }
|
|
1451
|
+
.linteau-progress-title { font-size: 13px; font-weight: 600; margin-bottom: 12px; color: var(--linteau-foreground) }
|
|
1452
|
+
.linteau-progress-track { width: 100%; height: 8px; border-radius: 999px; background: var(--linteau-secondary); overflow: hidden; position: relative }
|
|
1453
|
+
.linteau-progress-fill { height: 100%; width: 0%; background: var(--linteau-primary); border-radius: 999px; transition: width 180ms ease }
|
|
1454
|
+
.linteau-progress-fill.linteau-indeterminate { width: 40% !important; animation: linteau-progress-slide 1.1s ease-in-out infinite }
|
|
1455
|
+
@keyframes linteau-progress-slide { 0% { transform: translateX(-100%) } 100% { transform: translateX(250%) } }
|
|
1456
|
+
.linteau-progress-percent { margin-top: 8px; font-size: 12px; color: var(--linteau-muted-foreground); text-align: right; font-variant-numeric: tabular-nums }
|
|
1457
|
+
`;
|
|
1458
|
+
function ProgressBar(options = {}) {
|
|
1459
|
+
assertDom("ProgressBar");
|
|
1460
|
+
ensureComponentStyles("progress-bar", progressBarCss);
|
|
1461
|
+
const { title = "", value = 0, indeterminate = false, visible = false } = options;
|
|
1462
|
+
const root = el("div", cx(px("progress-overlay"), !visible && px("hidden")));
|
|
1463
|
+
const card = el("div", px("progress-card"));
|
|
1464
|
+
const titleEl = el("div", px("progress-title"));
|
|
1465
|
+
titleEl.textContent = title;
|
|
1466
|
+
const track = el("div", px("progress-track"));
|
|
1467
|
+
const fill = el("div", cx(px("progress-fill"), indeterminate && px("indeterminate")));
|
|
1468
|
+
track.appendChild(fill);
|
|
1469
|
+
const percentLabel = el("div", px("progress-percent"));
|
|
1470
|
+
percentLabel.textContent = indeterminate ? "Working..." : `${Math.round(value)}%`;
|
|
1471
|
+
card.append(titleEl, track, percentLabel);
|
|
1472
|
+
root.appendChild(card);
|
|
1473
|
+
fill.style.width = `${Math.max(0, Math.min(100, value))}%`;
|
|
1474
|
+
const api = {
|
|
1475
|
+
setValue(percent, label) {
|
|
1476
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
1477
|
+
fill.classList.remove(px("indeterminate"));
|
|
1478
|
+
fill.style.width = `${clamped}%`;
|
|
1479
|
+
percentLabel.textContent = label ?? `${Math.round(clamped)}%`;
|
|
1480
|
+
},
|
|
1481
|
+
setIndeterminate(value2) {
|
|
1482
|
+
fill.classList.toggle(px("indeterminate"), value2);
|
|
1483
|
+
if (value2) percentLabel.textContent = "Working...";
|
|
1484
|
+
},
|
|
1485
|
+
setTitle(newTitle) {
|
|
1486
|
+
titleEl.textContent = newTitle;
|
|
1487
|
+
},
|
|
1488
|
+
show() {
|
|
1489
|
+
root.classList.remove(px("hidden"));
|
|
1490
|
+
},
|
|
1491
|
+
hide() {
|
|
1492
|
+
root.classList.add(px("hidden"));
|
|
1493
|
+
},
|
|
1494
|
+
destroy() {
|
|
1495
|
+
root.remove();
|
|
1496
|
+
}
|
|
1497
|
+
};
|
|
1498
|
+
return attachController(root, api);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/components/feedback/Skeleton.ts
|
|
1502
|
+
var skeletonCss = `
|
|
1503
|
+
.linteau-skeleton { background: linear-gradient(90deg, var(--linteau-secondary) 25%, var(--linteau-accent) 37%, var(--linteau-secondary) 63%); background-size: 400% 100%; animation: linteau-skeleton-shine 1.4s ease infinite; border-radius: var(--linteau-radius-sm) }
|
|
1504
|
+
@keyframes linteau-skeleton-shine { 0% { background-position: 100% 50% } 100% { background-position: 0 50% } }
|
|
1505
|
+
`;
|
|
1506
|
+
function Skeleton(options = {}) {
|
|
1507
|
+
assertDom("Skeleton");
|
|
1508
|
+
ensureComponentStyles("skeleton", skeletonCss);
|
|
1509
|
+
const root = el("div", px("skeleton"));
|
|
1510
|
+
root.style.width = typeof options.width === "number" ? `${options.width}px` : options.width ?? "100%";
|
|
1511
|
+
root.style.height = typeof options.height === "number" ? `${options.height}px` : options.height ?? "1em";
|
|
1512
|
+
if (options.radius) root.style.borderRadius = options.radius;
|
|
1513
|
+
const api = {
|
|
1514
|
+
destroy() {
|
|
1515
|
+
root.remove();
|
|
1516
|
+
}
|
|
1517
|
+
};
|
|
1518
|
+
return attachController(root, api);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// src/components/feedback/Spinner.ts
|
|
1522
|
+
var spinnerCss = `
|
|
1523
|
+
.linteau-spinner { animation: linteau-spin 0.8s linear infinite; color: var(--linteau-muted-foreground) }
|
|
1524
|
+
@keyframes linteau-spin { to { transform: rotate(360deg) } }
|
|
1525
|
+
`;
|
|
1526
|
+
function Spinner(options = {}) {
|
|
1527
|
+
assertDom("Spinner");
|
|
1528
|
+
ensureComponentStyles("spinner", spinnerCss);
|
|
1529
|
+
const size = options.size ?? 16;
|
|
1530
|
+
const svg = Icon(icons.loader, size, { className: px("spinner"), color: options.color });
|
|
1531
|
+
const api = {
|
|
1532
|
+
setSize(newSize) {
|
|
1533
|
+
svg.setAttribute("width", String(newSize));
|
|
1534
|
+
svg.setAttribute("height", String(newSize));
|
|
1535
|
+
},
|
|
1536
|
+
destroy() {
|
|
1537
|
+
svg.remove();
|
|
1538
|
+
}
|
|
1539
|
+
};
|
|
1540
|
+
return attachController(svg, api);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// src/components/feedback/Toast.ts
|
|
1544
|
+
var toastCss = `
|
|
1545
|
+
.linteau-toast-viewport { position: fixed; bottom: 16px; right: 16px; display: flex; flex-direction: column; gap: 8px; z-index: 1400; width: min(340px, 92vw) }
|
|
1546
|
+
.linteau-toast { display: flex; gap: 8px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-left: 3px solid var(--linteau-primary); border-radius: var(--linteau-radius-md); padding: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4) }
|
|
1547
|
+
.linteau-toast-success { border-left-color: var(--linteau-success) }
|
|
1548
|
+
.linteau-toast-warning { border-left-color: var(--linteau-warning) }
|
|
1549
|
+
.linteau-toast-destructive { border-left-color: var(--linteau-destructive) }
|
|
1550
|
+
.linteau-toast-body { flex: 1 }
|
|
1551
|
+
.linteau-toast-title { font-size: 12px; font-weight: 600 }
|
|
1552
|
+
.linteau-toast-description { font-size: 12px; color: var(--linteau-muted-foreground); margin-top: 2px }
|
|
1553
|
+
.linteau-toast-close { background: transparent; border: none; color: var(--linteau-muted-foreground); cursor: pointer; display: flex; align-self: flex-start }
|
|
1554
|
+
`;
|
|
1555
|
+
var idCounter = 0;
|
|
1556
|
+
function createToastManager() {
|
|
1557
|
+
assertDom("ToastManager");
|
|
1558
|
+
ensureComponentStyles("toast", toastCss);
|
|
1559
|
+
let viewport = null;
|
|
1560
|
+
const timers = /* @__PURE__ */ new Map();
|
|
1561
|
+
function ensureViewport() {
|
|
1562
|
+
if (!viewport) {
|
|
1563
|
+
viewport = el("div", px("toast-viewport"));
|
|
1564
|
+
document.body.appendChild(viewport);
|
|
1565
|
+
}
|
|
1566
|
+
return viewport;
|
|
1567
|
+
}
|
|
1568
|
+
function dismiss(id) {
|
|
1569
|
+
const timer = timers.get(id);
|
|
1570
|
+
if (timer) clearTimeout(timer);
|
|
1571
|
+
timers.delete(id);
|
|
1572
|
+
document.getElementById(id)?.remove();
|
|
1573
|
+
}
|
|
1574
|
+
function show(options) {
|
|
1575
|
+
const id = `linteau-toast-${++idCounter}`;
|
|
1576
|
+
const variant = options.variant ?? "default";
|
|
1577
|
+
const duration = options.duration ?? 4e3;
|
|
1578
|
+
const toastEl = el("div", cx(px("toast"), px(`toast-${variant}`)));
|
|
1579
|
+
toastEl.id = id;
|
|
1580
|
+
const body = el("div", px("toast-body"));
|
|
1581
|
+
if (options.title) {
|
|
1582
|
+
const title = el("div", px("toast-title"));
|
|
1583
|
+
title.textContent = options.title;
|
|
1584
|
+
body.appendChild(title);
|
|
1585
|
+
}
|
|
1586
|
+
if (options.description) {
|
|
1587
|
+
const description = el("div", px("toast-description"));
|
|
1588
|
+
description.textContent = options.description;
|
|
1589
|
+
body.appendChild(description);
|
|
1590
|
+
}
|
|
1591
|
+
const closeButton = el("button", px("toast-close"));
|
|
1592
|
+
closeButton.type = "button";
|
|
1593
|
+
const closeIcon = Icon(icons.x, 14);
|
|
1594
|
+
if (closeIcon) closeButton.appendChild(closeIcon);
|
|
1595
|
+
closeButton.addEventListener("click", () => dismiss(id));
|
|
1596
|
+
toastEl.append(body, closeButton);
|
|
1597
|
+
ensureViewport().appendChild(toastEl);
|
|
1598
|
+
if (duration > 0) timers.set(id, setTimeout(() => dismiss(id), duration));
|
|
1599
|
+
return id;
|
|
1600
|
+
}
|
|
1601
|
+
return {
|
|
1602
|
+
show,
|
|
1603
|
+
dismiss,
|
|
1604
|
+
clear() {
|
|
1605
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
1606
|
+
timers.clear();
|
|
1607
|
+
viewport?.replaceChildren();
|
|
1608
|
+
},
|
|
1609
|
+
destroy() {
|
|
1610
|
+
timers.forEach((timer) => clearTimeout(timer));
|
|
1611
|
+
timers.clear();
|
|
1612
|
+
viewport?.remove();
|
|
1613
|
+
viewport = null;
|
|
1614
|
+
}
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
var toast = createToastManager();
|
|
1618
|
+
|
|
1619
|
+
// src/components/field/shared.ts
|
|
1620
|
+
var fieldBaseCss = `
|
|
1621
|
+
.linteau-field { display: flex; flex-direction: column; gap: 6px; width: 100% }
|
|
1622
|
+
.linteau-field-row { display: flex; align-items: center; justify-content: space-between }
|
|
1623
|
+
.linteau-field-label { display: flex; align-items: center; gap: 6px; color: var(--linteau-foreground) }
|
|
1624
|
+
.linteau-field-value { color: var(--linteau-muted-foreground); font-variant-numeric: tabular-nums }
|
|
1625
|
+
.linteau-field-header { display: flex; justify-content: space-between; align-items: center; font-size: 12px }
|
|
1626
|
+
`;
|
|
1627
|
+
|
|
1628
|
+
// src/components/field/ColorField.ts
|
|
1629
|
+
var swatchCss = `
|
|
1630
|
+
.linteau-swatch { --linteau-swatch-color: #ffffff; width: 28px; height: 28px; border-radius: var(--linteau-radius-sm); background: var(--linteau-swatch-color); border: 1px solid var(--linteau-border); cursor: pointer; display: block; position: relative; overflow: hidden; transition: transform 120ms ease }
|
|
1631
|
+
.linteau-swatch:hover { transform: scale(1.06) }
|
|
1632
|
+
.linteau-swatch input[type='color'] { position: absolute; inset: 0; opacity: 0; cursor: pointer }
|
|
1633
|
+
`;
|
|
1634
|
+
function ColorField(options) {
|
|
1635
|
+
assertDom("ColorField");
|
|
1636
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
1637
|
+
ensureComponentStyles("swatch", swatchCss);
|
|
1638
|
+
let value = options.value ?? options.defaultValue ?? "#ffffff";
|
|
1639
|
+
const onChange = options.onChange;
|
|
1640
|
+
const root = el("div", px("field-row"));
|
|
1641
|
+
const labelWrap = el("div", px("field-label"));
|
|
1642
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
1643
|
+
const labelText = document.createElement("span");
|
|
1644
|
+
labelText.textContent = options.label;
|
|
1645
|
+
labelWrap.appendChild(labelText);
|
|
1646
|
+
const swatch = el("label", px("swatch"));
|
|
1647
|
+
swatch.style.setProperty("--linteau-swatch-color", value);
|
|
1648
|
+
const input = el("input", void 0);
|
|
1649
|
+
input.type = "color";
|
|
1650
|
+
input.value = value;
|
|
1651
|
+
input.disabled = options.disabled ?? false;
|
|
1652
|
+
const listener = () => {
|
|
1653
|
+
value = input.value;
|
|
1654
|
+
swatch.style.setProperty("--linteau-swatch-color", value);
|
|
1655
|
+
onChange?.(value);
|
|
1656
|
+
};
|
|
1657
|
+
input.addEventListener("input", listener);
|
|
1658
|
+
swatch.appendChild(input);
|
|
1659
|
+
root.append(labelWrap, swatch);
|
|
1660
|
+
const api = {
|
|
1661
|
+
getValue() {
|
|
1662
|
+
return value;
|
|
1663
|
+
},
|
|
1664
|
+
setValue(newValue) {
|
|
1665
|
+
value = newValue;
|
|
1666
|
+
input.value = newValue;
|
|
1667
|
+
swatch.style.setProperty("--linteau-swatch-color", newValue);
|
|
1668
|
+
},
|
|
1669
|
+
setDisabled(disabled) {
|
|
1670
|
+
input.disabled = disabled;
|
|
1671
|
+
},
|
|
1672
|
+
destroy() {
|
|
1673
|
+
input.removeEventListener("input", listener);
|
|
1674
|
+
root.remove();
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
return attachController(root, api);
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// src/components/field/textInput.css.ts
|
|
1681
|
+
var textInputCss = `
|
|
1682
|
+
.linteau-text-input, .linteau-textarea {
|
|
1683
|
+
width: 100%;
|
|
1684
|
+
background: var(--linteau-secondary);
|
|
1685
|
+
color: var(--linteau-foreground);
|
|
1686
|
+
border: 1px solid var(--linteau-border);
|
|
1687
|
+
border-radius: var(--linteau-radius-sm);
|
|
1688
|
+
padding: 8px 10px;
|
|
1689
|
+
font-size: 12px;
|
|
1690
|
+
font-family: inherit;
|
|
1691
|
+
transition: border-color 120ms ease;
|
|
1692
|
+
}
|
|
1693
|
+
.linteau-text-input:focus, .linteau-textarea:focus {
|
|
1694
|
+
outline: none;
|
|
1695
|
+
border-color: var(--linteau-ring);
|
|
1696
|
+
box-shadow: 0 0 0 2px color-mix(in oklch, var(--linteau-ring) 20%, transparent);
|
|
1697
|
+
}
|
|
1698
|
+
.linteau-text-input:disabled, .linteau-textarea:disabled { opacity: 0.5; cursor: not-allowed }
|
|
1699
|
+
.linteau-textarea { resize: vertical; min-height: 72px }
|
|
1700
|
+
`;
|
|
1701
|
+
|
|
1702
|
+
// src/components/field/NumberField.ts
|
|
1703
|
+
function NumberField(options = {}) {
|
|
1704
|
+
assertDom("NumberField");
|
|
1705
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
1706
|
+
ensureComponentStyles("text-input", textInputCss);
|
|
1707
|
+
const onChange = options.onChange;
|
|
1708
|
+
const root = el("div", px("field"));
|
|
1709
|
+
if (options.label) {
|
|
1710
|
+
const header = el("div", px("field-header"));
|
|
1711
|
+
const labelWrap = el("div", px("field-label"));
|
|
1712
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
1713
|
+
const labelText = document.createElement("span");
|
|
1714
|
+
labelText.textContent = options.label;
|
|
1715
|
+
labelWrap.appendChild(labelText);
|
|
1716
|
+
header.appendChild(labelWrap);
|
|
1717
|
+
root.appendChild(header);
|
|
1718
|
+
}
|
|
1719
|
+
const input = el("input", px("text-input"));
|
|
1720
|
+
input.type = "number";
|
|
1721
|
+
if (options.min !== void 0) input.min = String(options.min);
|
|
1722
|
+
if (options.max !== void 0) input.max = String(options.max);
|
|
1723
|
+
input.step = String(options.step ?? 1);
|
|
1724
|
+
input.value = String(options.value ?? options.defaultValue ?? 0);
|
|
1725
|
+
input.disabled = options.disabled ?? false;
|
|
1726
|
+
function clamp(value) {
|
|
1727
|
+
let result = value;
|
|
1728
|
+
if (options.min !== void 0) result = Math.max(options.min, result);
|
|
1729
|
+
if (options.max !== void 0) result = Math.min(options.max, result);
|
|
1730
|
+
return result;
|
|
1731
|
+
}
|
|
1732
|
+
const listener = () => {
|
|
1733
|
+
const parsed = Number(input.value);
|
|
1734
|
+
if (!Number.isNaN(parsed)) onChange?.(parsed);
|
|
1735
|
+
};
|
|
1736
|
+
input.addEventListener("input", listener);
|
|
1737
|
+
root.appendChild(input);
|
|
1738
|
+
const api = {
|
|
1739
|
+
getValue() {
|
|
1740
|
+
return Number(input.value);
|
|
1741
|
+
},
|
|
1742
|
+
setValue(value) {
|
|
1743
|
+
input.value = String(clamp(value));
|
|
1744
|
+
},
|
|
1745
|
+
setDisabled(disabled) {
|
|
1746
|
+
input.disabled = disabled;
|
|
1747
|
+
},
|
|
1748
|
+
destroy() {
|
|
1749
|
+
input.removeEventListener("input", listener);
|
|
1750
|
+
root.remove();
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
return attachController(root, api);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// src/components/field/TextArea.ts
|
|
1757
|
+
function TextArea(options = {}) {
|
|
1758
|
+
assertDom("TextArea");
|
|
1759
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
1760
|
+
ensureComponentStyles("text-input", textInputCss);
|
|
1761
|
+
const onChange = options.onChange;
|
|
1762
|
+
const root = el("div", px("field"));
|
|
1763
|
+
if (options.label) {
|
|
1764
|
+
const header = el("div", px("field-header"));
|
|
1765
|
+
const labelWrap = el("div", px("field-label"));
|
|
1766
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
1767
|
+
const labelText = document.createElement("span");
|
|
1768
|
+
labelText.textContent = options.label;
|
|
1769
|
+
labelWrap.appendChild(labelText);
|
|
1770
|
+
header.appendChild(labelWrap);
|
|
1771
|
+
root.appendChild(header);
|
|
1772
|
+
}
|
|
1773
|
+
const textarea = el("textarea", px("textarea"));
|
|
1774
|
+
textarea.placeholder = options.placeholder ?? "";
|
|
1775
|
+
textarea.rows = options.rows ?? 3;
|
|
1776
|
+
textarea.value = options.value ?? options.defaultValue ?? "";
|
|
1777
|
+
textarea.disabled = options.disabled ?? false;
|
|
1778
|
+
const listener = () => onChange?.(textarea.value);
|
|
1779
|
+
textarea.addEventListener("input", listener);
|
|
1780
|
+
root.appendChild(textarea);
|
|
1781
|
+
const api = {
|
|
1782
|
+
getValue() {
|
|
1783
|
+
return textarea.value;
|
|
1784
|
+
},
|
|
1785
|
+
setValue(value) {
|
|
1786
|
+
textarea.value = value;
|
|
1787
|
+
},
|
|
1788
|
+
setDisabled(disabled) {
|
|
1789
|
+
textarea.disabled = disabled;
|
|
1790
|
+
},
|
|
1791
|
+
destroy() {
|
|
1792
|
+
textarea.removeEventListener("input", listener);
|
|
1793
|
+
root.remove();
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
return attachController(root, api);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// src/components/field/TextField.ts
|
|
1800
|
+
function TextField(options = {}) {
|
|
1801
|
+
assertDom("TextField");
|
|
1802
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
1803
|
+
ensureComponentStyles("text-input", textInputCss);
|
|
1804
|
+
const onChange = options.onChange;
|
|
1805
|
+
const root = el("div", px("field"));
|
|
1806
|
+
if (options.label) {
|
|
1807
|
+
const header = el("div", px("field-header"));
|
|
1808
|
+
const labelWrap = el("div", px("field-label"));
|
|
1809
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
1810
|
+
const labelText = document.createElement("span");
|
|
1811
|
+
labelText.textContent = options.label;
|
|
1812
|
+
labelWrap.appendChild(labelText);
|
|
1813
|
+
header.appendChild(labelWrap);
|
|
1814
|
+
root.appendChild(header);
|
|
1815
|
+
}
|
|
1816
|
+
const input = el("input", px("text-input"));
|
|
1817
|
+
input.type = options.type ?? "text";
|
|
1818
|
+
input.placeholder = options.placeholder ?? "";
|
|
1819
|
+
input.value = options.value ?? options.defaultValue ?? "";
|
|
1820
|
+
input.disabled = options.disabled ?? false;
|
|
1821
|
+
const inputListener = () => onChange?.(input.value);
|
|
1822
|
+
const keyListener = (event) => {
|
|
1823
|
+
if (event.key === "Enter") options.onEnter?.(input.value);
|
|
1824
|
+
};
|
|
1825
|
+
input.addEventListener("input", inputListener);
|
|
1826
|
+
input.addEventListener("keydown", keyListener);
|
|
1827
|
+
root.appendChild(input);
|
|
1828
|
+
const api = {
|
|
1829
|
+
getValue() {
|
|
1830
|
+
return input.value;
|
|
1831
|
+
},
|
|
1832
|
+
setValue(value) {
|
|
1833
|
+
input.value = value;
|
|
1834
|
+
},
|
|
1835
|
+
setDisabled(disabled) {
|
|
1836
|
+
input.disabled = disabled;
|
|
1837
|
+
},
|
|
1838
|
+
focus() {
|
|
1839
|
+
input.focus();
|
|
1840
|
+
},
|
|
1841
|
+
destroy() {
|
|
1842
|
+
input.removeEventListener("input", inputListener);
|
|
1843
|
+
input.removeEventListener("keydown", keyListener);
|
|
1844
|
+
root.remove();
|
|
1845
|
+
}
|
|
1846
|
+
};
|
|
1847
|
+
return attachController(root, api);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
// src/core/content.ts
|
|
1851
|
+
function fillContent(target, content) {
|
|
1852
|
+
target.replaceChildren();
|
|
1853
|
+
if (content instanceof HTMLElement) target.appendChild(content);
|
|
1854
|
+
else target.textContent = content;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/components/hovercard/HoverCard.ts
|
|
1858
|
+
var hoverCardCss = `
|
|
1859
|
+
.linteau-hover-card { position: fixed; z-index: 1200; width: 280px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-md); padding: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); font-size: 12px; color: var(--linteau-foreground) }
|
|
1860
|
+
`;
|
|
1861
|
+
function HoverCard(options) {
|
|
1862
|
+
assertDom("HoverCard");
|
|
1863
|
+
ensureComponentStyles("hover-card", hoverCardCss);
|
|
1864
|
+
const { anchor, placement = "bottom" } = options;
|
|
1865
|
+
const openDelay = options.openDelay ?? 300;
|
|
1866
|
+
const closeDelay = options.closeDelay ?? 150;
|
|
1867
|
+
let openTimer = null;
|
|
1868
|
+
let closeTimer = null;
|
|
1869
|
+
let isVisible = false;
|
|
1870
|
+
const card = el("div", cx(px("hover-card"), px("hidden")));
|
|
1871
|
+
fillContent(card, options.content);
|
|
1872
|
+
function clearTimers() {
|
|
1873
|
+
if (openTimer) clearTimeout(openTimer);
|
|
1874
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
1875
|
+
openTimer = null;
|
|
1876
|
+
closeTimer = null;
|
|
1877
|
+
}
|
|
1878
|
+
function showNow() {
|
|
1879
|
+
clearTimers();
|
|
1880
|
+
if (isVisible) return;
|
|
1881
|
+
isVisible = true;
|
|
1882
|
+
document.body.appendChild(card);
|
|
1883
|
+
card.classList.remove(px("hidden"));
|
|
1884
|
+
positionFloating(anchor, card, placement);
|
|
1885
|
+
}
|
|
1886
|
+
function hideNow() {
|
|
1887
|
+
clearTimers();
|
|
1888
|
+
if (!isVisible) return;
|
|
1889
|
+
isVisible = false;
|
|
1890
|
+
card.classList.add(px("hidden"));
|
|
1891
|
+
card.remove();
|
|
1892
|
+
}
|
|
1893
|
+
function scheduleShow() {
|
|
1894
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
1895
|
+
closeTimer = null;
|
|
1896
|
+
if (isVisible || openTimer) return;
|
|
1897
|
+
openTimer = setTimeout(showNow, openDelay);
|
|
1898
|
+
}
|
|
1899
|
+
function scheduleHide() {
|
|
1900
|
+
if (openTimer) clearTimeout(openTimer);
|
|
1901
|
+
openTimer = null;
|
|
1902
|
+
if (!isVisible || closeTimer) return;
|
|
1903
|
+
closeTimer = setTimeout(hideNow, closeDelay);
|
|
1904
|
+
}
|
|
1905
|
+
anchor.addEventListener("mouseenter", scheduleShow);
|
|
1906
|
+
anchor.addEventListener("mouseleave", scheduleHide);
|
|
1907
|
+
anchor.addEventListener("focus", scheduleShow);
|
|
1908
|
+
anchor.addEventListener("blur", scheduleHide);
|
|
1909
|
+
card.addEventListener("mouseenter", () => {
|
|
1910
|
+
if (closeTimer) clearTimeout(closeTimer);
|
|
1911
|
+
closeTimer = null;
|
|
1912
|
+
});
|
|
1913
|
+
card.addEventListener("mouseleave", scheduleHide);
|
|
1914
|
+
return {
|
|
1915
|
+
setContent(content) {
|
|
1916
|
+
fillContent(card, content);
|
|
1917
|
+
},
|
|
1918
|
+
open: showNow,
|
|
1919
|
+
close: hideNow,
|
|
1920
|
+
destroy() {
|
|
1921
|
+
clearTimers();
|
|
1922
|
+
anchor.removeEventListener("mouseenter", scheduleShow);
|
|
1923
|
+
anchor.removeEventListener("mouseleave", scheduleHide);
|
|
1924
|
+
anchor.removeEventListener("focus", scheduleShow);
|
|
1925
|
+
anchor.removeEventListener("blur", scheduleHide);
|
|
1926
|
+
card.remove();
|
|
1927
|
+
}
|
|
1928
|
+
};
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// src/components/input/ChipInput.ts
|
|
1932
|
+
var chipInputCss = `
|
|
1933
|
+
.linteau-chip-field { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px; background: var(--linteau-secondary); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-sm) }
|
|
1934
|
+
.linteau-chip-field:focus-within { border-color: var(--linteau-ring) }
|
|
1935
|
+
.linteau-chip { display: inline-flex; align-items: center; gap: 4px; background: var(--linteau-accent); color: var(--linteau-accent-foreground); border-radius: 999px; padding: 3px 8px; font-size: 11px }
|
|
1936
|
+
.linteau-chip-remove { background: transparent; border: none; color: inherit; cursor: pointer; display: flex; padding: 0 }
|
|
1937
|
+
.linteau-chip-input { flex: 1; min-width: 80px; background: transparent; border: none; color: var(--linteau-foreground); font-size: 12px; font-family: inherit; outline: none }
|
|
1938
|
+
`;
|
|
1939
|
+
function ChipInput(options = {}) {
|
|
1940
|
+
assertDom("ChipInput");
|
|
1941
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
1942
|
+
ensureComponentStyles("chip-input", chipInputCss);
|
|
1943
|
+
let chips = options.value ?? options.defaultValue ?? [];
|
|
1944
|
+
const onChange = options.onChange;
|
|
1945
|
+
const cleanupChipListeners = [];
|
|
1946
|
+
const root = el("div", px("field"));
|
|
1947
|
+
if (options.label) {
|
|
1948
|
+
const header = el("div", px("field-header"));
|
|
1949
|
+
const labelWrap = el("div", px("field-label"));
|
|
1950
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
1951
|
+
const labelText = document.createElement("span");
|
|
1952
|
+
labelText.textContent = options.label;
|
|
1953
|
+
labelWrap.appendChild(labelText);
|
|
1954
|
+
header.appendChild(labelWrap);
|
|
1955
|
+
root.appendChild(header);
|
|
1956
|
+
}
|
|
1957
|
+
const fieldWrap = el("div", px("chip-field"));
|
|
1958
|
+
const input = el("input", px("chip-input"));
|
|
1959
|
+
input.type = "text";
|
|
1960
|
+
input.placeholder = options.placeholder ?? "";
|
|
1961
|
+
fieldWrap.appendChild(input);
|
|
1962
|
+
function renderChips() {
|
|
1963
|
+
Array.from(fieldWrap.children).forEach((child) => {
|
|
1964
|
+
if (child !== input) child.remove();
|
|
1965
|
+
});
|
|
1966
|
+
cleanupChipListeners.splice(0).forEach((fn) => fn());
|
|
1967
|
+
chips.forEach((value, index) => {
|
|
1968
|
+
const chip = el("span", px("chip"));
|
|
1969
|
+
const label = document.createElement("span");
|
|
1970
|
+
label.textContent = value;
|
|
1971
|
+
const removeButton = el("button", px("chip-remove"));
|
|
1972
|
+
removeButton.type = "button";
|
|
1973
|
+
const removeIcon = Icon(icons.x, 10);
|
|
1974
|
+
if (removeIcon) removeButton.appendChild(removeIcon);
|
|
1975
|
+
const listener = () => {
|
|
1976
|
+
chips = chips.filter((_, i) => i !== index);
|
|
1977
|
+
renderChips();
|
|
1978
|
+
onChange?.(chips);
|
|
1979
|
+
};
|
|
1980
|
+
removeButton.addEventListener("click", listener);
|
|
1981
|
+
cleanupChipListeners.push(() => removeButton.removeEventListener("click", listener));
|
|
1982
|
+
chip.append(label, removeButton);
|
|
1983
|
+
fieldWrap.insertBefore(chip, input);
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
renderChips();
|
|
1987
|
+
const keyListener = (event) => {
|
|
1988
|
+
if (event.key === "Enter" && input.value.trim()) {
|
|
1989
|
+
event.preventDefault();
|
|
1990
|
+
chips = [...chips, input.value.trim()];
|
|
1991
|
+
input.value = "";
|
|
1992
|
+
renderChips();
|
|
1993
|
+
onChange?.(chips);
|
|
1994
|
+
} else if (event.key === "Backspace" && !input.value && chips.length > 0) {
|
|
1995
|
+
chips = chips.slice(0, -1);
|
|
1996
|
+
renderChips();
|
|
1997
|
+
onChange?.(chips);
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
input.addEventListener("keydown", keyListener);
|
|
2001
|
+
root.appendChild(fieldWrap);
|
|
2002
|
+
const api = {
|
|
2003
|
+
getValue() {
|
|
2004
|
+
return chips;
|
|
2005
|
+
},
|
|
2006
|
+
setValue(newValue) {
|
|
2007
|
+
chips = newValue;
|
|
2008
|
+
renderChips();
|
|
2009
|
+
},
|
|
2010
|
+
destroy() {
|
|
2011
|
+
input.removeEventListener("keydown", keyListener);
|
|
2012
|
+
cleanupChipListeners.splice(0).forEach((fn) => fn());
|
|
2013
|
+
root.remove();
|
|
2014
|
+
}
|
|
2015
|
+
};
|
|
2016
|
+
return attachController(root, api);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
// src/components/select/Combobox.ts
|
|
2020
|
+
var comboboxCss = `
|
|
2021
|
+
.linteau-combobox { position: relative; display: flex; align-items: center }
|
|
2022
|
+
.linteau-combobox-input { padding-right: 28px }
|
|
2023
|
+
.linteau-combobox-chevron { position: absolute; right: 8px; pointer-events: none; color: var(--linteau-muted-foreground) }
|
|
2024
|
+
.linteau-combobox-list { position: fixed; z-index: 1200; min-width: 180px; max-height: 220px; overflow-y: auto; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-md); padding: 4px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4) }
|
|
2025
|
+
.linteau-combobox-option { padding: 6px 8px; border-radius: var(--linteau-radius-sm); font-size: 12px; cursor: pointer; color: var(--linteau-foreground) }
|
|
2026
|
+
.linteau-combobox-option-highlighted, .linteau-combobox-option:hover { background: var(--linteau-accent) }
|
|
2027
|
+
`;
|
|
2028
|
+
function Combobox(options) {
|
|
2029
|
+
assertDom("Combobox");
|
|
2030
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
2031
|
+
ensureComponentStyles("text-input", textInputCss);
|
|
2032
|
+
ensureComponentStyles("combobox", comboboxCss);
|
|
2033
|
+
let items = options.options;
|
|
2034
|
+
let value = options.value ?? options.defaultValue ?? "";
|
|
2035
|
+
let query = items.find((item) => item.value === value)?.label ?? "";
|
|
2036
|
+
let isOpen = false;
|
|
2037
|
+
let highlighted = 0;
|
|
2038
|
+
let unbindOutside = null;
|
|
2039
|
+
const onChange = options.onChange;
|
|
2040
|
+
const root = el("div", px("field"));
|
|
2041
|
+
const header = el("div", px("field-header"));
|
|
2042
|
+
const labelWrap = el("div", px("field-label"));
|
|
2043
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
2044
|
+
const labelText = document.createElement("span");
|
|
2045
|
+
labelText.textContent = options.label;
|
|
2046
|
+
labelWrap.appendChild(labelText);
|
|
2047
|
+
header.appendChild(labelWrap);
|
|
2048
|
+
const wrap = el("div", px("combobox"));
|
|
2049
|
+
const input = el("input", cx(px("text-input"), px("combobox-input")));
|
|
2050
|
+
input.type = "text";
|
|
2051
|
+
input.placeholder = options.placeholder ?? "";
|
|
2052
|
+
input.value = query;
|
|
2053
|
+
input.disabled = options.disabled ?? false;
|
|
2054
|
+
const chevron = Icon(icons.chevronDown, 12, { className: px("combobox-chevron") });
|
|
2055
|
+
const list = el("div", cx(px("combobox-list"), px("hidden")));
|
|
2056
|
+
function filtered() {
|
|
2057
|
+
const normalized = query.trim().toLowerCase();
|
|
2058
|
+
if (!normalized) return items;
|
|
2059
|
+
return items.filter((item) => item.label.toLowerCase().includes(normalized));
|
|
2060
|
+
}
|
|
2061
|
+
function renderList() {
|
|
2062
|
+
list.replaceChildren();
|
|
2063
|
+
filtered().forEach((item, index) => {
|
|
2064
|
+
const optionEl = el("div", cx(px("combobox-option"), index === highlighted && px("combobox-option-highlighted")));
|
|
2065
|
+
optionEl.textContent = item.label;
|
|
2066
|
+
optionEl.addEventListener("mousedown", (event) => {
|
|
2067
|
+
event.preventDefault();
|
|
2068
|
+
select(item);
|
|
2069
|
+
});
|
|
2070
|
+
list.appendChild(optionEl);
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
function select(item) {
|
|
2074
|
+
value = item.value;
|
|
2075
|
+
query = item.label;
|
|
2076
|
+
input.value = query;
|
|
2077
|
+
close();
|
|
2078
|
+
onChange?.(value);
|
|
2079
|
+
}
|
|
2080
|
+
function open() {
|
|
2081
|
+
if (isOpen || input.disabled) return;
|
|
2082
|
+
isOpen = true;
|
|
2083
|
+
highlighted = 0;
|
|
2084
|
+
document.body.appendChild(list);
|
|
2085
|
+
list.classList.remove(px("hidden"));
|
|
2086
|
+
renderList();
|
|
2087
|
+
positionFloating(wrap, list, "bottom");
|
|
2088
|
+
unbindOutside = onClickOutside([wrap, list], close);
|
|
2089
|
+
}
|
|
2090
|
+
function close() {
|
|
2091
|
+
if (!isOpen) return;
|
|
2092
|
+
isOpen = false;
|
|
2093
|
+
list.classList.add(px("hidden"));
|
|
2094
|
+
list.remove();
|
|
2095
|
+
unbindOutside?.();
|
|
2096
|
+
unbindOutside = null;
|
|
2097
|
+
}
|
|
2098
|
+
const inputListener = () => {
|
|
2099
|
+
query = input.value;
|
|
2100
|
+
highlighted = 0;
|
|
2101
|
+
if (!isOpen) open();
|
|
2102
|
+
else renderList();
|
|
2103
|
+
};
|
|
2104
|
+
const keyListener = (event) => {
|
|
2105
|
+
const results = filtered();
|
|
2106
|
+
if (event.key === "ArrowDown") {
|
|
2107
|
+
event.preventDefault();
|
|
2108
|
+
highlighted = Math.min(highlighted + 1, results.length - 1);
|
|
2109
|
+
renderList();
|
|
2110
|
+
} else if (event.key === "ArrowUp") {
|
|
2111
|
+
event.preventDefault();
|
|
2112
|
+
highlighted = Math.max(highlighted - 1, 0);
|
|
2113
|
+
renderList();
|
|
2114
|
+
} else if (event.key === "Enter") {
|
|
2115
|
+
event.preventDefault();
|
|
2116
|
+
const item = results[highlighted];
|
|
2117
|
+
if (item) select(item);
|
|
2118
|
+
} else if (event.key === "Escape") {
|
|
2119
|
+
close();
|
|
2120
|
+
}
|
|
2121
|
+
};
|
|
2122
|
+
const focusListener = () => open();
|
|
2123
|
+
input.addEventListener("input", inputListener);
|
|
2124
|
+
input.addEventListener("keydown", keyListener);
|
|
2125
|
+
input.addEventListener("focus", focusListener);
|
|
2126
|
+
wrap.append(input, chevron);
|
|
2127
|
+
root.append(header, wrap);
|
|
2128
|
+
const api = {
|
|
2129
|
+
getValue() {
|
|
2130
|
+
return value;
|
|
2131
|
+
},
|
|
2132
|
+
setValue(newValue) {
|
|
2133
|
+
value = newValue;
|
|
2134
|
+
query = items.find((item) => item.value === newValue)?.label ?? "";
|
|
2135
|
+
input.value = query;
|
|
2136
|
+
},
|
|
2137
|
+
setOptions(newOptions) {
|
|
2138
|
+
items = newOptions;
|
|
2139
|
+
if (isOpen) renderList();
|
|
2140
|
+
},
|
|
2141
|
+
open,
|
|
2142
|
+
close,
|
|
2143
|
+
destroy() {
|
|
2144
|
+
input.removeEventListener("input", inputListener);
|
|
2145
|
+
input.removeEventListener("keydown", keyListener);
|
|
2146
|
+
input.removeEventListener("focus", focusListener);
|
|
2147
|
+
unbindOutside?.();
|
|
2148
|
+
list.remove();
|
|
2149
|
+
root.remove();
|
|
2150
|
+
}
|
|
2151
|
+
};
|
|
2152
|
+
return attachController(root, api);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// src/components/input/DatePicker.ts
|
|
2156
|
+
var calendarCss = `
|
|
2157
|
+
.linteau-calendar { position: fixed; z-index: 1200; width: 280px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); padding: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4) }
|
|
2158
|
+
.linteau-calendar-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px }
|
|
2159
|
+
.linteau-calendar-nav { background: transparent; border: none; color: var(--linteau-foreground); cursor: pointer; display: flex; padding: 4px; border-radius: var(--linteau-radius-sm) }
|
|
2160
|
+
.linteau-calendar-nav:hover:not(:disabled) { background: var(--linteau-accent) }
|
|
2161
|
+
.linteau-calendar-nav:disabled { opacity: 0.35; cursor: not-allowed }
|
|
2162
|
+
.linteau-calendar-month { font-size: 12px; font-weight: 600 }
|
|
2163
|
+
.linteau-calendar-weekdays { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-bottom: 2px }
|
|
2164
|
+
.linteau-calendar-weekday { height: 20px; display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: var(--linteau-muted-foreground); text-transform: uppercase }
|
|
2165
|
+
.linteau-calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px }
|
|
2166
|
+
.linteau-calendar-day { height: 28px; display: flex; align-items: center; justify-content: center; font-size: 11px; border-radius: var(--linteau-radius-sm); background: transparent; border: none; color: var(--linteau-foreground); cursor: pointer; font-family: inherit }
|
|
2167
|
+
.linteau-calendar-day:hover:not(:disabled) { background: var(--linteau-accent) }
|
|
2168
|
+
.linteau-calendar-day:disabled { opacity: 0.35; cursor: not-allowed }
|
|
2169
|
+
.linteau-calendar-day:focus-visible { outline: 2px solid var(--linteau-ring); outline-offset: 1px }
|
|
2170
|
+
.linteau-calendar-day-outside { opacity: 0.35 }
|
|
2171
|
+
.linteau-calendar-day-selected { background: var(--linteau-primary); color: var(--linteau-primary-foreground) }
|
|
2172
|
+
.linteau-calendar-day-today:not(.linteau-calendar-day-selected) { box-shadow: inset 0 0 0 1px var(--linteau-ring) }
|
|
2173
|
+
.linteau-calendar-day-in-range { background: var(--linteau-accent); border-radius: 0 }
|
|
2174
|
+
.linteau-calendar-day-range-start { background: var(--linteau-primary); color: var(--linteau-primary-foreground); border-radius: var(--linteau-radius-sm) 0 0 var(--linteau-radius-sm) }
|
|
2175
|
+
.linteau-calendar-day-range-end { background: var(--linteau-primary); color: var(--linteau-primary-foreground); border-radius: 0 var(--linteau-radius-sm) var(--linteau-radius-sm) 0 }
|
|
2176
|
+
.linteau-calendar-footer { display: flex; gap: 6px; margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--linteau-border) }
|
|
2177
|
+
.linteau-calendar-footer-button { flex: 1; padding: 6px 8px; font-size: 11px; border-radius: var(--linteau-radius-sm); border: 1px solid var(--linteau-border); background: transparent; color: var(--linteau-foreground); cursor: pointer; font-family: inherit }
|
|
2178
|
+
.linteau-calendar-footer-button:hover { background: var(--linteau-accent) }
|
|
2179
|
+
.linteau-combobox-input-clearable { padding-right: 46px }
|
|
2180
|
+
.linteau-combobox-clear { position: absolute; right: 26px; color: var(--linteau-muted-foreground); background: transparent; border: none; cursor: pointer; display: flex; padding: 2px }
|
|
2181
|
+
.linteau-combobox-clear:hover { color: var(--linteau-foreground) }
|
|
2182
|
+
`;
|
|
2183
|
+
function isSameDay(a, b) {
|
|
2184
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
2185
|
+
}
|
|
2186
|
+
function startOfMonth(date) {
|
|
2187
|
+
return new Date(date.getFullYear(), date.getMonth(), 1);
|
|
2188
|
+
}
|
|
2189
|
+
function addMonths(date, count) {
|
|
2190
|
+
return new Date(date.getFullYear(), date.getMonth() + count, 1);
|
|
2191
|
+
}
|
|
2192
|
+
function addDays(date, count) {
|
|
2193
|
+
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + count);
|
|
2194
|
+
}
|
|
2195
|
+
function isRange(value) {
|
|
2196
|
+
return Boolean(value) && typeof value === "object" && "from" in value;
|
|
2197
|
+
}
|
|
2198
|
+
function buildWeekdayLabels(weekStartsOn) {
|
|
2199
|
+
const base = new Date(2021, 0, 3);
|
|
2200
|
+
const labels = [];
|
|
2201
|
+
for (let i = 0; i < 7; i++) {
|
|
2202
|
+
const day = new Date(base.getFullYear(), base.getMonth(), base.getDate() + i + weekStartsOn);
|
|
2203
|
+
labels.push(day.toLocaleDateString(void 0, { weekday: "short" }).slice(0, 2));
|
|
2204
|
+
}
|
|
2205
|
+
return labels;
|
|
2206
|
+
}
|
|
2207
|
+
function getCalendarDays(viewDate, weekStartsOn) {
|
|
2208
|
+
const year = viewDate.getFullYear();
|
|
2209
|
+
const month = viewDate.getMonth();
|
|
2210
|
+
const firstOfMonth = new Date(year, month, 1);
|
|
2211
|
+
const lastOfMonth = new Date(year, month + 1, 0);
|
|
2212
|
+
const leadingOffset = (firstOfMonth.getDay() - weekStartsOn + 7) % 7;
|
|
2213
|
+
const trailingOffset = (weekStartsOn + 6 - lastOfMonth.getDay() + 7) % 7;
|
|
2214
|
+
const days = [];
|
|
2215
|
+
for (let i = leadingOffset; i > 0; i--) days.push({ date: new Date(year, month, 1 - i), inCurrentMonth: false });
|
|
2216
|
+
for (let day = 1; day <= lastOfMonth.getDate(); day++) days.push({ date: new Date(year, month, day), inCurrentMonth: true });
|
|
2217
|
+
for (let i = 1; i <= trailingOffset; i++) days.push({ date: new Date(year, month + 1, i), inCurrentMonth: false });
|
|
2218
|
+
return days;
|
|
2219
|
+
}
|
|
2220
|
+
function DatePicker(options = {}) {
|
|
2221
|
+
assertDom("DatePicker");
|
|
2222
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
2223
|
+
ensureComponentStyles("text-input", textInputCss);
|
|
2224
|
+
ensureComponentStyles("combobox", comboboxCss);
|
|
2225
|
+
ensureComponentStyles("calendar", calendarCss);
|
|
2226
|
+
const mode = options.mode ?? "single";
|
|
2227
|
+
const weekStartsOn = options.weekStartsOn ?? 0;
|
|
2228
|
+
const min = options.min;
|
|
2229
|
+
const max = options.max;
|
|
2230
|
+
const format = options.format ?? ((date) => date.toLocaleDateString());
|
|
2231
|
+
const onChange = options.onChange;
|
|
2232
|
+
const showTodayButton = options.showTodayButton ?? true;
|
|
2233
|
+
const showClearButton = options.showClearButton ?? true;
|
|
2234
|
+
let value = options.value ?? options.defaultValue;
|
|
2235
|
+
let rangeAnchor = null;
|
|
2236
|
+
let viewDate = (isRange(value) ? value.from : value) ?? /* @__PURE__ */ new Date();
|
|
2237
|
+
let focusedDate = viewDate;
|
|
2238
|
+
let isOpen = false;
|
|
2239
|
+
let unbindOutside = null;
|
|
2240
|
+
let unbindEscape = null;
|
|
2241
|
+
const dayButtonsByTime = /* @__PURE__ */ new Map();
|
|
2242
|
+
function formatValue(v) {
|
|
2243
|
+
if (!v) return "";
|
|
2244
|
+
if (isRange(v)) return v.to ? `${format(v.from)} - ${format(v.to)}` : format(v.from);
|
|
2245
|
+
return format(v);
|
|
2246
|
+
}
|
|
2247
|
+
function isDisabledDate(date) {
|
|
2248
|
+
return Boolean(min && date < min || max && date > max);
|
|
2249
|
+
}
|
|
2250
|
+
const root = el("div", px("field"));
|
|
2251
|
+
if (options.label) {
|
|
2252
|
+
const header = el("div", px("field-header"));
|
|
2253
|
+
const labelWrap = el("div", px("field-label"));
|
|
2254
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
2255
|
+
const labelText = document.createElement("span");
|
|
2256
|
+
labelText.textContent = options.label;
|
|
2257
|
+
labelWrap.appendChild(labelText);
|
|
2258
|
+
header.appendChild(labelWrap);
|
|
2259
|
+
root.appendChild(header);
|
|
2260
|
+
}
|
|
2261
|
+
const wrap = el("div", px("combobox"));
|
|
2262
|
+
const input = el("input", cx(px("text-input"), px("combobox-input")));
|
|
2263
|
+
input.type = "text";
|
|
2264
|
+
input.readOnly = true;
|
|
2265
|
+
input.value = formatValue(value);
|
|
2266
|
+
input.placeholder = options.placeholder ?? (mode === "range" ? "Select a date range" : "Select a date");
|
|
2267
|
+
input.disabled = options.disabled ?? false;
|
|
2268
|
+
const clearButton = el("button", px("combobox-clear"));
|
|
2269
|
+
clearButton.type = "button";
|
|
2270
|
+
const clearIcon = Icon(icons.x, 12);
|
|
2271
|
+
if (clearIcon) clearButton.appendChild(clearIcon);
|
|
2272
|
+
const calendarIcon = Icon(icons.calendar, 14, { className: px("combobox-chevron") });
|
|
2273
|
+
wrap.append(input, clearButton, calendarIcon);
|
|
2274
|
+
root.appendChild(wrap);
|
|
2275
|
+
function syncClearButton() {
|
|
2276
|
+
const hasValue = Boolean(value);
|
|
2277
|
+
clearButton.style.display = hasValue && showClearButton ? "flex" : "none";
|
|
2278
|
+
input.classList.toggle(px("combobox-input-clearable"), hasValue && showClearButton);
|
|
2279
|
+
}
|
|
2280
|
+
syncClearButton();
|
|
2281
|
+
const calendar2 = el("div", cx(px("calendar"), px("hidden")));
|
|
2282
|
+
calendar2.setAttribute("role", "application");
|
|
2283
|
+
function focusDayButton(date) {
|
|
2284
|
+
dayButtonsByTime.get(new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime())?.focus();
|
|
2285
|
+
}
|
|
2286
|
+
function updateHoverPreview(hovered) {
|
|
2287
|
+
if (mode !== "range" || !rangeAnchor) return;
|
|
2288
|
+
for (const [time, button] of dayButtonsByTime) {
|
|
2289
|
+
const date = new Date(time);
|
|
2290
|
+
const inPreview = hovered ? date >= rangeAnchor && date <= hovered || date <= rangeAnchor && date >= hovered : false;
|
|
2291
|
+
button.classList.toggle(px("calendar-day-in-range"), inPreview && !isSameDay(date, rangeAnchor));
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
function handleDayClick(date) {
|
|
2295
|
+
if (isDisabledDate(date)) return;
|
|
2296
|
+
if (mode === "range") {
|
|
2297
|
+
if (!rangeAnchor) {
|
|
2298
|
+
rangeAnchor = date;
|
|
2299
|
+
value = { from: date };
|
|
2300
|
+
input.value = formatValue(value);
|
|
2301
|
+
renderCalendar();
|
|
2302
|
+
} else {
|
|
2303
|
+
const from = rangeAnchor <= date ? rangeAnchor : date;
|
|
2304
|
+
const to = rangeAnchor <= date ? date : rangeAnchor;
|
|
2305
|
+
value = { from, to };
|
|
2306
|
+
rangeAnchor = null;
|
|
2307
|
+
input.value = formatValue(value);
|
|
2308
|
+
syncClearButton();
|
|
2309
|
+
onChange?.(value);
|
|
2310
|
+
close();
|
|
2311
|
+
}
|
|
2312
|
+
} else {
|
|
2313
|
+
value = date;
|
|
2314
|
+
input.value = formatValue(value);
|
|
2315
|
+
syncClearButton();
|
|
2316
|
+
onChange?.(value);
|
|
2317
|
+
close();
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
function renderCalendar() {
|
|
2321
|
+
calendar2.replaceChildren();
|
|
2322
|
+
dayButtonsByTime.clear();
|
|
2323
|
+
const header = el("div", px("calendar-header"));
|
|
2324
|
+
const prevButton = el("button", px("calendar-nav"));
|
|
2325
|
+
prevButton.type = "button";
|
|
2326
|
+
prevButton.setAttribute("aria-label", "Previous month");
|
|
2327
|
+
const prevIcon = Icon(icons.chevronLeft, 14);
|
|
2328
|
+
if (prevIcon) prevButton.appendChild(prevIcon);
|
|
2329
|
+
prevButton.disabled = Boolean(min && addMonths(viewDate, -1) < startOfMonth(min));
|
|
2330
|
+
const nextButton = el("button", px("calendar-nav"));
|
|
2331
|
+
nextButton.type = "button";
|
|
2332
|
+
nextButton.setAttribute("aria-label", "Next month");
|
|
2333
|
+
const nextIcon = Icon(icons.chevronRight, 14);
|
|
2334
|
+
if (nextIcon) nextButton.appendChild(nextIcon);
|
|
2335
|
+
nextButton.disabled = Boolean(max && addMonths(viewDate, 1) > startOfMonth(max));
|
|
2336
|
+
const monthLabel = el("div", px("calendar-month"));
|
|
2337
|
+
monthLabel.textContent = viewDate.toLocaleDateString(void 0, { month: "long", year: "numeric" });
|
|
2338
|
+
prevButton.addEventListener("click", () => {
|
|
2339
|
+
viewDate = addMonths(viewDate, -1);
|
|
2340
|
+
renderCalendar();
|
|
2341
|
+
});
|
|
2342
|
+
nextButton.addEventListener("click", () => {
|
|
2343
|
+
viewDate = addMonths(viewDate, 1);
|
|
2344
|
+
renderCalendar();
|
|
2345
|
+
});
|
|
2346
|
+
header.append(prevButton, monthLabel, nextButton);
|
|
2347
|
+
const weekdaysRow = el("div", px("calendar-weekdays"));
|
|
2348
|
+
for (const label of buildWeekdayLabels(weekStartsOn)) {
|
|
2349
|
+
const cell = el("div", px("calendar-weekday"));
|
|
2350
|
+
cell.textContent = label;
|
|
2351
|
+
weekdaysRow.appendChild(cell);
|
|
2352
|
+
}
|
|
2353
|
+
const grid = el("div", px("calendar-grid"));
|
|
2354
|
+
const today = /* @__PURE__ */ new Date();
|
|
2355
|
+
for (const { date, inCurrentMonth } of getCalendarDays(viewDate, weekStartsOn)) {
|
|
2356
|
+
const dayButton = el("button", px("calendar-day"));
|
|
2357
|
+
dayButton.type = "button";
|
|
2358
|
+
dayButton.textContent = String(date.getDate());
|
|
2359
|
+
dayButton.disabled = isDisabledDate(date);
|
|
2360
|
+
dayButton.tabIndex = isSameDay(date, focusedDate) ? 0 : -1;
|
|
2361
|
+
if (!inCurrentMonth) dayButton.classList.add(px("calendar-day-outside"));
|
|
2362
|
+
if (isSameDay(date, today)) dayButton.classList.add(px("calendar-day-today"));
|
|
2363
|
+
const currentValue = value;
|
|
2364
|
+
if (currentValue && !isRange(currentValue) && isSameDay(date, currentValue)) {
|
|
2365
|
+
dayButton.classList.add(px("calendar-day-selected"));
|
|
2366
|
+
dayButton.setAttribute("aria-selected", "true");
|
|
2367
|
+
}
|
|
2368
|
+
if (currentValue && isRange(currentValue)) {
|
|
2369
|
+
if (isSameDay(date, currentValue.from)) dayButton.classList.add(px("calendar-day-range-start"), px("calendar-day-selected"));
|
|
2370
|
+
if (currentValue.to && isSameDay(date, currentValue.to)) dayButton.classList.add(px("calendar-day-range-end"), px("calendar-day-selected"));
|
|
2371
|
+
if (currentValue.to && date > currentValue.from && date < currentValue.to) dayButton.classList.add(px("calendar-day-in-range"));
|
|
2372
|
+
}
|
|
2373
|
+
dayButton.addEventListener("click", () => {
|
|
2374
|
+
focusedDate = date;
|
|
2375
|
+
handleDayClick(date);
|
|
2376
|
+
});
|
|
2377
|
+
dayButton.addEventListener("mouseenter", () => updateHoverPreview(date));
|
|
2378
|
+
dayButtonsByTime.set(new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(), dayButton);
|
|
2379
|
+
grid.appendChild(dayButton);
|
|
2380
|
+
}
|
|
2381
|
+
calendar2.append(header, weekdaysRow, grid);
|
|
2382
|
+
if (showTodayButton || showClearButton) {
|
|
2383
|
+
const footer = el("div", px("calendar-footer"));
|
|
2384
|
+
if (showTodayButton) {
|
|
2385
|
+
const todayButton = el("button", px("calendar-footer-button"));
|
|
2386
|
+
todayButton.type = "button";
|
|
2387
|
+
todayButton.textContent = "Today";
|
|
2388
|
+
todayButton.addEventListener("click", () => {
|
|
2389
|
+
viewDate = startOfMonth(/* @__PURE__ */ new Date());
|
|
2390
|
+
focusedDate = /* @__PURE__ */ new Date();
|
|
2391
|
+
renderCalendar();
|
|
2392
|
+
focusDayButton(focusedDate);
|
|
2393
|
+
});
|
|
2394
|
+
footer.appendChild(todayButton);
|
|
2395
|
+
}
|
|
2396
|
+
if (showClearButton) {
|
|
2397
|
+
const clearFooterButton = el("button", px("calendar-footer-button"));
|
|
2398
|
+
clearFooterButton.type = "button";
|
|
2399
|
+
clearFooterButton.textContent = "Clear";
|
|
2400
|
+
clearFooterButton.addEventListener("click", () => {
|
|
2401
|
+
value = void 0;
|
|
2402
|
+
rangeAnchor = null;
|
|
2403
|
+
input.value = "";
|
|
2404
|
+
syncClearButton();
|
|
2405
|
+
onChange?.(void 0);
|
|
2406
|
+
renderCalendar();
|
|
2407
|
+
});
|
|
2408
|
+
footer.appendChild(clearFooterButton);
|
|
2409
|
+
}
|
|
2410
|
+
calendar2.appendChild(footer);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
function handleKeydown(event) {
|
|
2414
|
+
const deltas = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
2415
|
+
if (event.key in deltas) {
|
|
2416
|
+
event.preventDefault();
|
|
2417
|
+
focusedDate = addDays(focusedDate, deltas[event.key]);
|
|
2418
|
+
if (focusedDate.getMonth() !== viewDate.getMonth() || focusedDate.getFullYear() !== viewDate.getFullYear()) viewDate = startOfMonth(focusedDate);
|
|
2419
|
+
renderCalendar();
|
|
2420
|
+
focusDayButton(focusedDate);
|
|
2421
|
+
} else if (event.key === "PageUp") {
|
|
2422
|
+
event.preventDefault();
|
|
2423
|
+
viewDate = addMonths(viewDate, -1);
|
|
2424
|
+
renderCalendar();
|
|
2425
|
+
} else if (event.key === "PageDown") {
|
|
2426
|
+
event.preventDefault();
|
|
2427
|
+
viewDate = addMonths(viewDate, 1);
|
|
2428
|
+
renderCalendar();
|
|
2429
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
2430
|
+
event.preventDefault();
|
|
2431
|
+
handleDayClick(focusedDate);
|
|
2432
|
+
}
|
|
2433
|
+
}
|
|
2434
|
+
function open() {
|
|
2435
|
+
if (isOpen || input.disabled) return;
|
|
2436
|
+
isOpen = true;
|
|
2437
|
+
viewDate = (isRange(value) ? value.from : value) ?? viewDate;
|
|
2438
|
+
focusedDate = viewDate;
|
|
2439
|
+
document.body.appendChild(calendar2);
|
|
2440
|
+
calendar2.classList.remove(px("hidden"));
|
|
2441
|
+
renderCalendar();
|
|
2442
|
+
positionFloating(wrap, calendar2, "bottom");
|
|
2443
|
+
calendar2.addEventListener("keydown", handleKeydown);
|
|
2444
|
+
unbindOutside = onClickOutside([wrap, calendar2], close);
|
|
2445
|
+
unbindEscape = onEscapeKey(close);
|
|
2446
|
+
}
|
|
2447
|
+
function close() {
|
|
2448
|
+
if (!isOpen) return;
|
|
2449
|
+
isOpen = false;
|
|
2450
|
+
rangeAnchor = null;
|
|
2451
|
+
calendar2.classList.add(px("hidden"));
|
|
2452
|
+
calendar2.removeEventListener("keydown", handleKeydown);
|
|
2453
|
+
calendar2.remove();
|
|
2454
|
+
unbindOutside?.();
|
|
2455
|
+
unbindOutside = null;
|
|
2456
|
+
unbindEscape?.();
|
|
2457
|
+
unbindEscape = null;
|
|
2458
|
+
}
|
|
2459
|
+
const toggleListener = () => isOpen ? close() : open();
|
|
2460
|
+
input.addEventListener("click", toggleListener);
|
|
2461
|
+
calendarIcon.addEventListener("click", toggleListener);
|
|
2462
|
+
clearButton.addEventListener("click", (event) => {
|
|
2463
|
+
event.stopPropagation();
|
|
2464
|
+
value = void 0;
|
|
2465
|
+
rangeAnchor = null;
|
|
2466
|
+
input.value = "";
|
|
2467
|
+
syncClearButton();
|
|
2468
|
+
onChange?.(void 0);
|
|
2469
|
+
if (isOpen) renderCalendar();
|
|
2470
|
+
});
|
|
2471
|
+
const api = {
|
|
2472
|
+
getValue() {
|
|
2473
|
+
return value;
|
|
2474
|
+
},
|
|
2475
|
+
setValue(newValue) {
|
|
2476
|
+
value = newValue;
|
|
2477
|
+
input.value = formatValue(newValue);
|
|
2478
|
+
syncClearButton();
|
|
2479
|
+
},
|
|
2480
|
+
open,
|
|
2481
|
+
close,
|
|
2482
|
+
isOpen() {
|
|
2483
|
+
return isOpen;
|
|
2484
|
+
},
|
|
2485
|
+
destroy() {
|
|
2486
|
+
input.removeEventListener("click", toggleListener);
|
|
2487
|
+
calendarIcon.removeEventListener("click", toggleListener);
|
|
2488
|
+
unbindOutside?.();
|
|
2489
|
+
unbindEscape?.();
|
|
2490
|
+
calendar2.removeEventListener("keydown", handleKeydown);
|
|
2491
|
+
calendar2.remove();
|
|
2492
|
+
root.remove();
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
return attachController(root, api);
|
|
2496
|
+
}
|
|
2497
|
+
|
|
2498
|
+
// src/components/input/FileDropzone.ts
|
|
2499
|
+
var dropzoneCss = `
|
|
2500
|
+
.linteau-dropzone { border: 1px dashed var(--linteau-border); border-radius: var(--linteau-radius-lg); padding: 24px; display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; cursor: pointer; color: var(--linteau-muted-foreground); transition: border-color 120ms ease, background 120ms ease }
|
|
2501
|
+
.linteau-dropzone:hover, .linteau-dropzone-active { border-color: var(--linteau-ring); background: var(--linteau-accent) }
|
|
2502
|
+
.linteau-dropzone-disabled { opacity: 0.5; cursor: not-allowed }
|
|
2503
|
+
.linteau-dropzone-label { font-size: 12px }
|
|
2504
|
+
`;
|
|
2505
|
+
function FileDropzone(options) {
|
|
2506
|
+
assertDom("FileDropzone");
|
|
2507
|
+
ensureComponentStyles("dropzone", dropzoneCss);
|
|
2508
|
+
const disabled = options.disabled ?? false;
|
|
2509
|
+
const root = el("div", cx(px("dropzone"), disabled && px("dropzone-disabled")));
|
|
2510
|
+
const icon = Icon(icons.upload, 22, { className: px("icon") });
|
|
2511
|
+
if (icon) root.appendChild(icon);
|
|
2512
|
+
const label = el("div", px("dropzone-label"));
|
|
2513
|
+
label.textContent = options.label ?? "Drop files here or click to browse";
|
|
2514
|
+
root.appendChild(label);
|
|
2515
|
+
const input = document.createElement("input");
|
|
2516
|
+
input.type = "file";
|
|
2517
|
+
input.style.display = "none";
|
|
2518
|
+
input.multiple = options.multiple ?? false;
|
|
2519
|
+
if (options.accept) input.accept = options.accept;
|
|
2520
|
+
input.disabled = disabled;
|
|
2521
|
+
function emit(fileList) {
|
|
2522
|
+
if (!fileList || fileList.length === 0) return;
|
|
2523
|
+
options.onFiles(Array.from(fileList));
|
|
2524
|
+
}
|
|
2525
|
+
const clickListener = () => {
|
|
2526
|
+
if (!disabled) input.click();
|
|
2527
|
+
};
|
|
2528
|
+
const changeListener = () => emit(input.files);
|
|
2529
|
+
const dragOverListener = (event) => {
|
|
2530
|
+
event.preventDefault();
|
|
2531
|
+
if (!disabled) root.classList.add(px("dropzone-active"));
|
|
2532
|
+
};
|
|
2533
|
+
const dragLeaveListener = () => root.classList.remove(px("dropzone-active"));
|
|
2534
|
+
const dropListener = (event) => {
|
|
2535
|
+
event.preventDefault();
|
|
2536
|
+
root.classList.remove(px("dropzone-active"));
|
|
2537
|
+
if (!disabled) emit(event.dataTransfer?.files ?? null);
|
|
2538
|
+
};
|
|
2539
|
+
root.addEventListener("click", clickListener);
|
|
2540
|
+
root.addEventListener("dragover", dragOverListener);
|
|
2541
|
+
root.addEventListener("dragleave", dragLeaveListener);
|
|
2542
|
+
root.addEventListener("drop", dropListener);
|
|
2543
|
+
input.addEventListener("change", changeListener);
|
|
2544
|
+
root.appendChild(input);
|
|
2545
|
+
const api = {
|
|
2546
|
+
reset() {
|
|
2547
|
+
input.value = "";
|
|
2548
|
+
},
|
|
2549
|
+
destroy() {
|
|
2550
|
+
root.removeEventListener("click", clickListener);
|
|
2551
|
+
root.removeEventListener("dragover", dragOverListener);
|
|
2552
|
+
root.removeEventListener("dragleave", dragLeaveListener);
|
|
2553
|
+
root.removeEventListener("drop", dropListener);
|
|
2554
|
+
input.removeEventListener("change", changeListener);
|
|
2555
|
+
root.remove();
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
return attachController(root, api);
|
|
2559
|
+
}
|
|
2560
|
+
|
|
2561
|
+
// src/components/layout/Card.ts
|
|
2562
|
+
var cardCss = `
|
|
2563
|
+
.linteau-card { border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); background: var(--linteau-card); color: var(--linteau-card-foreground); overflow: hidden }
|
|
2564
|
+
.linteau-card-header { padding: 14px 16px; border-bottom: 1px solid var(--linteau-border); font-weight: 600 }
|
|
2565
|
+
.linteau-card-body { padding: 16px }
|
|
2566
|
+
.linteau-card-footer { padding: 12px 16px; border-top: 1px solid var(--linteau-border) }
|
|
2567
|
+
`;
|
|
2568
|
+
function Card(options = {}) {
|
|
2569
|
+
assertDom("Card");
|
|
2570
|
+
ensureComponentStyles("card", cardCss);
|
|
2571
|
+
const root = el("div", px("card"));
|
|
2572
|
+
const headerEl = el("div", px("card-header"));
|
|
2573
|
+
const body = el("div", px("card-body"));
|
|
2574
|
+
const footerEl = el("div", px("card-footer"));
|
|
2575
|
+
function fill(target, content) {
|
|
2576
|
+
target.replaceChildren();
|
|
2577
|
+
if (content instanceof HTMLElement) target.appendChild(content);
|
|
2578
|
+
else if (typeof content === "string") target.textContent = content;
|
|
2579
|
+
}
|
|
2580
|
+
function syncVisibility(target, content) {
|
|
2581
|
+
target.style.display = content == null ? "none" : "";
|
|
2582
|
+
}
|
|
2583
|
+
fill(headerEl, options.header ?? null);
|
|
2584
|
+
fill(footerEl, options.footer ?? null);
|
|
2585
|
+
syncVisibility(headerEl, options.header ?? null);
|
|
2586
|
+
syncVisibility(footerEl, options.footer ?? null);
|
|
2587
|
+
root.append(headerEl, body, footerEl);
|
|
2588
|
+
const api = {
|
|
2589
|
+
body,
|
|
2590
|
+
setHeader(content) {
|
|
2591
|
+
fill(headerEl, content);
|
|
2592
|
+
syncVisibility(headerEl, content);
|
|
2593
|
+
},
|
|
2594
|
+
setFooter(content) {
|
|
2595
|
+
fill(footerEl, content);
|
|
2596
|
+
syncVisibility(footerEl, content);
|
|
2597
|
+
},
|
|
2598
|
+
destroy() {
|
|
2599
|
+
root.remove();
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
return attachController(root, api);
|
|
2603
|
+
}
|
|
2604
|
+
|
|
2605
|
+
// src/components/layout/Section.ts
|
|
2606
|
+
var sectionCss = `
|
|
2607
|
+
.linteau-section { border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); background: var(--linteau-muted); overflow: hidden; width: 100% }
|
|
2608
|
+
.linteau-section-header { display: flex; align-items: center; gap: 8px; padding: 10px 12px; border-bottom: 1px solid var(--linteau-border); color: var(--linteau-muted-foreground); cursor: default }
|
|
2609
|
+
.linteau-section-header.linteau-section-collapsible { cursor: pointer }
|
|
2610
|
+
.linteau-section-header h3 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600; flex: 1 }
|
|
2611
|
+
.linteau-section-actions { display: flex; align-items: center; gap: 6px }
|
|
2612
|
+
.linteau-section-body { padding: 12px; display: flex; flex-direction: column; gap: 12px }
|
|
2613
|
+
.linteau-section-body.linteau-collapsed { display: none }
|
|
2614
|
+
.linteau-section-chevron { transition: transform 150ms ease }
|
|
2615
|
+
.linteau-section-chevron.linteau-collapsed { transform: rotate(-90deg) }
|
|
2616
|
+
`;
|
|
2617
|
+
function Section(title, options = {}) {
|
|
2618
|
+
assertDom("Section");
|
|
2619
|
+
ensureComponentStyles("section", sectionCss);
|
|
2620
|
+
const { icon, collapsible = false, defaultCollapsed = false, actions = [] } = options;
|
|
2621
|
+
let collapsed = defaultCollapsed;
|
|
2622
|
+
const root = el("div", px("section"));
|
|
2623
|
+
const header = el("div", cx(px("section-header"), collapsible && px("section-collapsible")));
|
|
2624
|
+
const titleEl = document.createElement("h3");
|
|
2625
|
+
titleEl.textContent = title;
|
|
2626
|
+
const body = el("div", cx(px("section-body"), collapsed && px("collapsed")));
|
|
2627
|
+
const headerIcon = icon ? Icon(icon, 14, { className: px("icon") }) : null;
|
|
2628
|
+
const chevron = collapsible ? Icon(icons.chevronDown, 14, { className: cx(px("icon"), px("section-chevron"), collapsed && px("collapsed")) }) : null;
|
|
2629
|
+
const actionsWrap = el("div", px("section-actions"));
|
|
2630
|
+
actionsWrap.append(...actions);
|
|
2631
|
+
if (headerIcon) header.appendChild(headerIcon);
|
|
2632
|
+
header.appendChild(titleEl);
|
|
2633
|
+
if (actions.length) header.appendChild(actionsWrap);
|
|
2634
|
+
if (chevron) header.appendChild(chevron);
|
|
2635
|
+
root.append(header, body);
|
|
2636
|
+
function applyCollapsed() {
|
|
2637
|
+
body.classList.toggle(px("collapsed"), collapsed);
|
|
2638
|
+
chevron?.classList.toggle(px("collapsed"), collapsed);
|
|
2639
|
+
}
|
|
2640
|
+
const clickListener = () => {
|
|
2641
|
+
collapsed = !collapsed;
|
|
2642
|
+
applyCollapsed();
|
|
2643
|
+
};
|
|
2644
|
+
if (collapsible) header.addEventListener("click", clickListener);
|
|
2645
|
+
const api = {
|
|
2646
|
+
header,
|
|
2647
|
+
body,
|
|
2648
|
+
setTitle(newTitle) {
|
|
2649
|
+
titleEl.textContent = newTitle;
|
|
2650
|
+
},
|
|
2651
|
+
setCollapsed(value) {
|
|
2652
|
+
collapsed = value;
|
|
2653
|
+
applyCollapsed();
|
|
2654
|
+
},
|
|
2655
|
+
isCollapsed() {
|
|
2656
|
+
return collapsed;
|
|
2657
|
+
},
|
|
2658
|
+
destroy() {
|
|
2659
|
+
if (collapsible) header.removeEventListener("click", clickListener);
|
|
2660
|
+
root.remove();
|
|
2661
|
+
}
|
|
2662
|
+
};
|
|
2663
|
+
return attachController(root, api);
|
|
2664
|
+
}
|
|
2665
|
+
|
|
2666
|
+
// src/components/layout/Separator.ts
|
|
2667
|
+
var separatorCss = `
|
|
2668
|
+
.linteau-separator { background: var(--linteau-border); border: none; flex-shrink: 0 }
|
|
2669
|
+
.linteau-separator-horizontal { width: 100%; height: 1px }
|
|
2670
|
+
.linteau-separator-vertical { width: 1px; height: 100% }
|
|
2671
|
+
`;
|
|
2672
|
+
function Separator(options = {}) {
|
|
2673
|
+
assertDom("Separator");
|
|
2674
|
+
ensureComponentStyles("separator", separatorCss);
|
|
2675
|
+
let orientation = options.orientation ?? "horizontal";
|
|
2676
|
+
const hr = el("hr", cx(px("separator"), px(`separator-${orientation}`)));
|
|
2677
|
+
hr.setAttribute("role", "separator");
|
|
2678
|
+
const api = {
|
|
2679
|
+
setOrientation(newOrientation) {
|
|
2680
|
+
hr.classList.remove(px(`separator-${orientation}`));
|
|
2681
|
+
orientation = newOrientation;
|
|
2682
|
+
hr.classList.add(px(`separator-${orientation}`));
|
|
2683
|
+
},
|
|
2684
|
+
destroy() {
|
|
2685
|
+
hr.remove();
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
return attachController(hr, api);
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// src/components/misc/ModeCard.ts
|
|
2692
|
+
var modeCardCss = `
|
|
2693
|
+
.linteau-mode-card { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; padding: 14px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); color: var(--linteau-foreground); cursor: pointer; text-align: left; font-family: inherit; transition: border-color 120ms ease, transform 120ms ease }
|
|
2694
|
+
.linteau-mode-card:hover:not(.linteau-locked) { border-color: var(--linteau-ring); transform: translateY(-2px) }
|
|
2695
|
+
.linteau-mode-card.linteau-selected { border-color: var(--linteau-primary); box-shadow: 0 0 0 1px var(--linteau-primary) }
|
|
2696
|
+
.linteau-mode-card.linteau-locked { opacity: 0.45; cursor: not-allowed }
|
|
2697
|
+
.linteau-mode-card-icon { color: var(--linteau-foreground) }
|
|
2698
|
+
.linteau-mode-card-title { font-weight: 600; font-size: 13px }
|
|
2699
|
+
.linteau-mode-card-description { font-size: 11px; color: var(--linteau-muted-foreground) }
|
|
2700
|
+
`;
|
|
2701
|
+
function ModeCard(options) {
|
|
2702
|
+
assertDom("ModeCard");
|
|
2703
|
+
ensureComponentStyles("mode-card", modeCardCss);
|
|
2704
|
+
let locked = options.locked ?? false;
|
|
2705
|
+
let selected = options.selected ?? false;
|
|
2706
|
+
const lockedMessage = options.lockedMessage ?? "Coming in the next update";
|
|
2707
|
+
const root = el("button", px("mode-card"));
|
|
2708
|
+
root.type = "button";
|
|
2709
|
+
const iconWrap = el("div", px("mode-card-icon"));
|
|
2710
|
+
const textWrap = el("div", px("mode-card-text"));
|
|
2711
|
+
const titleEl = el("div", px("mode-card-title"));
|
|
2712
|
+
titleEl.textContent = options.label;
|
|
2713
|
+
const descEl = el("div", px("mode-card-description"));
|
|
2714
|
+
textWrap.append(titleEl, descEl);
|
|
2715
|
+
function render() {
|
|
2716
|
+
iconWrap.replaceChildren();
|
|
2717
|
+
const iconEl = Icon(locked ? icons.lock : options.icon, 20, { className: px("icon") });
|
|
2718
|
+
if (iconEl) iconWrap.appendChild(iconEl);
|
|
2719
|
+
descEl.textContent = locked ? lockedMessage : options.description;
|
|
2720
|
+
root.classList.toggle(px("locked"), locked);
|
|
2721
|
+
root.classList.toggle(px("selected"), selected);
|
|
2722
|
+
root.disabled = locked;
|
|
2723
|
+
}
|
|
2724
|
+
render();
|
|
2725
|
+
root.append(iconWrap, textWrap);
|
|
2726
|
+
const listener = () => {
|
|
2727
|
+
if (locked) return;
|
|
2728
|
+
options.onSelect?.();
|
|
2729
|
+
};
|
|
2730
|
+
root.addEventListener("click", listener);
|
|
2731
|
+
const api = {
|
|
2732
|
+
isSelected() {
|
|
2733
|
+
return selected;
|
|
2734
|
+
},
|
|
2735
|
+
setSelected(value) {
|
|
2736
|
+
selected = value;
|
|
2737
|
+
render();
|
|
2738
|
+
},
|
|
2739
|
+
setLocked(value) {
|
|
2740
|
+
locked = value;
|
|
2741
|
+
render();
|
|
2742
|
+
},
|
|
2743
|
+
destroy() {
|
|
2744
|
+
root.removeEventListener("click", listener);
|
|
2745
|
+
root.remove();
|
|
2746
|
+
}
|
|
2747
|
+
};
|
|
2748
|
+
return attachController(root, api);
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
// src/components/navigation/Accordion.ts
|
|
2752
|
+
var accordionCss = `
|
|
2753
|
+
.linteau-accordion { display: flex; flex-direction: column; gap: 8px; width: 100% }
|
|
2754
|
+
.linteau-accordion-item { border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); overflow: hidden }
|
|
2755
|
+
.linteau-accordion-header { display: flex; align-items: center; gap: 8px; width: 100%; padding: 12px; background: var(--linteau-muted); border: none; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--linteau-foreground); font-family: inherit }
|
|
2756
|
+
.linteau-accordion-chevron { margin-left: auto; transition: transform 150ms ease }
|
|
2757
|
+
.linteau-accordion-chevron.linteau-open { transform: rotate(180deg) }
|
|
2758
|
+
.linteau-accordion-content { padding: 12px }
|
|
2759
|
+
.linteau-accordion-content.linteau-collapsed { display: none }
|
|
2760
|
+
`;
|
|
2761
|
+
function Accordion(options) {
|
|
2762
|
+
assertDom("Accordion");
|
|
2763
|
+
ensureComponentStyles("accordion", accordionCss);
|
|
2764
|
+
const multiple = options.multiple ?? false;
|
|
2765
|
+
let openValues = new Set(options.defaultOpen ?? []);
|
|
2766
|
+
const onChange = options.onChange;
|
|
2767
|
+
const cleanupListeners = [];
|
|
2768
|
+
const root = el("div", px("accordion"));
|
|
2769
|
+
function setOpenState(value, shouldOpen) {
|
|
2770
|
+
if (shouldOpen) {
|
|
2771
|
+
if (!multiple) openValues.clear();
|
|
2772
|
+
openValues.add(value);
|
|
2773
|
+
} else {
|
|
2774
|
+
openValues.delete(value);
|
|
2775
|
+
}
|
|
2776
|
+
render();
|
|
2777
|
+
onChange?.(Array.from(openValues));
|
|
2778
|
+
}
|
|
2779
|
+
function render() {
|
|
2780
|
+
root.replaceChildren();
|
|
2781
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2782
|
+
for (const item of options.items) {
|
|
2783
|
+
const isOpen = openValues.has(item.value);
|
|
2784
|
+
const itemEl = el("div", px("accordion-item"));
|
|
2785
|
+
const header = el("button", px("accordion-header"));
|
|
2786
|
+
header.type = "button";
|
|
2787
|
+
if (item.icon) header.appendChild(Icon(item.icon, 14, { className: px("icon") }));
|
|
2788
|
+
const label = document.createElement("span");
|
|
2789
|
+
label.textContent = item.label;
|
|
2790
|
+
const chevron = Icon(icons.chevronDown, 14, { className: cx(px("accordion-chevron"), isOpen && px("open")) });
|
|
2791
|
+
header.append(label, chevron);
|
|
2792
|
+
const content = el("div", cx(px("accordion-content"), !isOpen && px("collapsed")));
|
|
2793
|
+
content.appendChild(item.content);
|
|
2794
|
+
const listener = () => setOpenState(item.value, !isOpen);
|
|
2795
|
+
header.addEventListener("click", listener);
|
|
2796
|
+
cleanupListeners.push(() => header.removeEventListener("click", listener));
|
|
2797
|
+
itemEl.append(header, content);
|
|
2798
|
+
root.appendChild(itemEl);
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
render();
|
|
2802
|
+
const api = {
|
|
2803
|
+
getOpen() {
|
|
2804
|
+
return Array.from(openValues);
|
|
2805
|
+
},
|
|
2806
|
+
open(value) {
|
|
2807
|
+
setOpenState(value, true);
|
|
2808
|
+
},
|
|
2809
|
+
close(value) {
|
|
2810
|
+
setOpenState(value, false);
|
|
2811
|
+
},
|
|
2812
|
+
toggle(value) {
|
|
2813
|
+
setOpenState(value, !openValues.has(value));
|
|
2814
|
+
},
|
|
2815
|
+
destroy() {
|
|
2816
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2817
|
+
root.remove();
|
|
2818
|
+
}
|
|
2819
|
+
};
|
|
2820
|
+
return attachController(root, api);
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
// src/components/navigation/Breadcrumb.ts
|
|
2824
|
+
var breadcrumbCss = `
|
|
2825
|
+
.linteau-breadcrumb { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--linteau-muted-foreground) }
|
|
2826
|
+
.linteau-breadcrumb-item { background: transparent; border: none; color: var(--linteau-muted-foreground); cursor: pointer; font: inherit; padding: 0 }
|
|
2827
|
+
.linteau-breadcrumb-item:hover { color: var(--linteau-foreground) }
|
|
2828
|
+
.linteau-breadcrumb-current { color: var(--linteau-foreground); font-weight: 500 }
|
|
2829
|
+
`;
|
|
2830
|
+
function Breadcrumb(options) {
|
|
2831
|
+
assertDom("Breadcrumb");
|
|
2832
|
+
ensureComponentStyles("breadcrumb", breadcrumbCss);
|
|
2833
|
+
const nav = el("nav", px("breadcrumb"));
|
|
2834
|
+
nav.setAttribute("aria-label", "breadcrumb");
|
|
2835
|
+
const cleanupListeners = [];
|
|
2836
|
+
function render(items) {
|
|
2837
|
+
nav.replaceChildren();
|
|
2838
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2839
|
+
items.forEach((item, index) => {
|
|
2840
|
+
const isLast = index === items.length - 1;
|
|
2841
|
+
if (isLast) {
|
|
2842
|
+
const current = el("span", px("breadcrumb-current"));
|
|
2843
|
+
current.textContent = item.label;
|
|
2844
|
+
nav.appendChild(current);
|
|
2845
|
+
} else {
|
|
2846
|
+
const button = el("button", px("breadcrumb-item"));
|
|
2847
|
+
button.type = "button";
|
|
2848
|
+
button.textContent = item.label;
|
|
2849
|
+
const listener = () => item.onClick?.();
|
|
2850
|
+
button.addEventListener("click", listener);
|
|
2851
|
+
cleanupListeners.push(() => button.removeEventListener("click", listener));
|
|
2852
|
+
nav.appendChild(button);
|
|
2853
|
+
const separator = Icon(icons.chevronRight, 12, { className: px("icon") });
|
|
2854
|
+
if (separator) nav.appendChild(separator);
|
|
2855
|
+
}
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
render(options.items);
|
|
2859
|
+
const api = {
|
|
2860
|
+
setItems(items) {
|
|
2861
|
+
render(items);
|
|
2862
|
+
},
|
|
2863
|
+
destroy() {
|
|
2864
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2865
|
+
nav.remove();
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2868
|
+
return attachController(nav, api);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/components/navigation/Pagination.ts
|
|
2872
|
+
var paginationCss = `
|
|
2873
|
+
.linteau-pagination { display: flex; align-items: center; gap: 4px }
|
|
2874
|
+
.linteau-pagination-item { min-width: 28px; height: 28px; padding: 0 6px; display: flex; align-items: center; justify-content: center; border-radius: var(--linteau-radius-sm); border: 1px solid transparent; background: transparent; color: var(--linteau-muted-foreground); font-size: 12px; cursor: pointer; font-family: inherit }
|
|
2875
|
+
.linteau-pagination-item:hover:not(:disabled) { background: var(--linteau-accent) }
|
|
2876
|
+
.linteau-pagination-item:disabled { opacity: 0.4; cursor: not-allowed }
|
|
2877
|
+
.linteau-pagination-item-active { background: var(--linteau-primary); color: var(--linteau-primary-foreground) }
|
|
2878
|
+
.linteau-pagination-ellipsis { color: var(--linteau-muted-foreground); padding: 0 4px }
|
|
2879
|
+
`;
|
|
2880
|
+
function buildRange(current, pageCount, siblingCount) {
|
|
2881
|
+
const totalVisible = siblingCount * 2 + 5;
|
|
2882
|
+
if (pageCount <= totalVisible) return Array.from({ length: pageCount }, (_, i) => i + 1);
|
|
2883
|
+
const leftSibling = Math.max(current - siblingCount, 1);
|
|
2884
|
+
const rightSibling = Math.min(current + siblingCount, pageCount);
|
|
2885
|
+
const showLeftEllipsis = leftSibling > 2;
|
|
2886
|
+
const showRightEllipsis = rightSibling < pageCount - 1;
|
|
2887
|
+
const range = [1];
|
|
2888
|
+
if (showLeftEllipsis) range.push("ellipsis");
|
|
2889
|
+
for (let page = Math.max(leftSibling, 2); page <= Math.min(rightSibling, pageCount - 1); page++) range.push(page);
|
|
2890
|
+
if (showRightEllipsis) range.push("ellipsis");
|
|
2891
|
+
if (pageCount > 1) range.push(pageCount);
|
|
2892
|
+
return range;
|
|
2893
|
+
}
|
|
2894
|
+
function Pagination(options) {
|
|
2895
|
+
assertDom("Pagination");
|
|
2896
|
+
ensureComponentStyles("pagination", paginationCss);
|
|
2897
|
+
let page = options.page ?? options.defaultPage ?? 1;
|
|
2898
|
+
let pageCount = options.pageCount;
|
|
2899
|
+
const siblingCount = options.siblingCount ?? 1;
|
|
2900
|
+
const onChange = options.onChange;
|
|
2901
|
+
const cleanupListeners = [];
|
|
2902
|
+
const root = el("div", px("pagination"));
|
|
2903
|
+
root.setAttribute("role", "navigation");
|
|
2904
|
+
function goTo(newPage) {
|
|
2905
|
+
page = Math.max(1, Math.min(pageCount, newPage));
|
|
2906
|
+
render();
|
|
2907
|
+
onChange?.(page);
|
|
2908
|
+
}
|
|
2909
|
+
function render() {
|
|
2910
|
+
root.replaceChildren();
|
|
2911
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2912
|
+
const prev = el("button", px("pagination-item"));
|
|
2913
|
+
prev.type = "button";
|
|
2914
|
+
const prevIcon = Icon(icons.chevronLeft, 14);
|
|
2915
|
+
if (prevIcon) prev.appendChild(prevIcon);
|
|
2916
|
+
prev.disabled = page <= 1;
|
|
2917
|
+
const prevListener = () => goTo(page - 1);
|
|
2918
|
+
prev.addEventListener("click", prevListener);
|
|
2919
|
+
cleanupListeners.push(() => prev.removeEventListener("click", prevListener));
|
|
2920
|
+
root.appendChild(prev);
|
|
2921
|
+
for (const entry of buildRange(page, pageCount, siblingCount)) {
|
|
2922
|
+
if (entry === "ellipsis") {
|
|
2923
|
+
root.appendChild(el("span", px("pagination-ellipsis")));
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
const button = el("button", cx(px("pagination-item"), entry === page && px("pagination-item-active")));
|
|
2927
|
+
button.type = "button";
|
|
2928
|
+
button.textContent = String(entry);
|
|
2929
|
+
const listener = () => goTo(entry);
|
|
2930
|
+
button.addEventListener("click", listener);
|
|
2931
|
+
cleanupListeners.push(() => button.removeEventListener("click", listener));
|
|
2932
|
+
root.appendChild(button);
|
|
2933
|
+
}
|
|
2934
|
+
const next = el("button", px("pagination-item"));
|
|
2935
|
+
next.type = "button";
|
|
2936
|
+
const nextIcon = Icon(icons.chevronRight, 14);
|
|
2937
|
+
if (nextIcon) next.appendChild(nextIcon);
|
|
2938
|
+
next.disabled = page >= pageCount;
|
|
2939
|
+
const nextListener = () => goTo(page + 1);
|
|
2940
|
+
next.addEventListener("click", nextListener);
|
|
2941
|
+
cleanupListeners.push(() => next.removeEventListener("click", nextListener));
|
|
2942
|
+
root.appendChild(next);
|
|
2943
|
+
}
|
|
2944
|
+
render();
|
|
2945
|
+
const api = {
|
|
2946
|
+
getPage() {
|
|
2947
|
+
return page;
|
|
2948
|
+
},
|
|
2949
|
+
setPage(newPage) {
|
|
2950
|
+
page = Math.max(1, Math.min(pageCount, newPage));
|
|
2951
|
+
render();
|
|
2952
|
+
},
|
|
2953
|
+
setPageCount(newPageCount) {
|
|
2954
|
+
pageCount = newPageCount;
|
|
2955
|
+
render();
|
|
2956
|
+
},
|
|
2957
|
+
destroy() {
|
|
2958
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2959
|
+
root.remove();
|
|
2960
|
+
}
|
|
2961
|
+
};
|
|
2962
|
+
return attachController(root, api);
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// src/components/navigation/Tabs.ts
|
|
2966
|
+
var tabsCss = `
|
|
2967
|
+
.linteau-tabs { display: flex; flex-direction: column; gap: 12px; width: 100% }
|
|
2968
|
+
.linteau-tabs-list { display: flex; gap: 4px; border-bottom: 1px solid var(--linteau-border) }
|
|
2969
|
+
.linteau-tabs-trigger { background: transparent; border: none; border-bottom: 2px solid transparent; padding: 8px 12px; font-size: 12px; font-weight: 500; color: var(--linteau-muted-foreground); cursor: pointer; display: flex; align-items: center; gap: 6px; font-family: inherit }
|
|
2970
|
+
.linteau-tabs-trigger:hover { color: var(--linteau-foreground) }
|
|
2971
|
+
.linteau-tabs-trigger-active { color: var(--linteau-foreground); border-bottom-color: var(--linteau-primary) }
|
|
2972
|
+
.linteau-tabs-panel { width: 100% }
|
|
2973
|
+
`;
|
|
2974
|
+
function Tabs(options) {
|
|
2975
|
+
assertDom("Tabs");
|
|
2976
|
+
ensureComponentStyles("tabs", tabsCss);
|
|
2977
|
+
let value = options.value ?? options.defaultValue ?? options.items[0]?.value ?? "";
|
|
2978
|
+
const onChange = options.onChange;
|
|
2979
|
+
const cleanupListeners = [];
|
|
2980
|
+
const root = el("div", px("tabs"));
|
|
2981
|
+
const list = el("div", px("tabs-list"));
|
|
2982
|
+
list.setAttribute("role", "tablist");
|
|
2983
|
+
const panel = el("div", px("tabs-panel"));
|
|
2984
|
+
function renderPanel() {
|
|
2985
|
+
const active = options.items.find((item) => item.value === value);
|
|
2986
|
+
panel.replaceChildren();
|
|
2987
|
+
if (active) panel.appendChild(active.content);
|
|
2988
|
+
}
|
|
2989
|
+
function renderTriggers() {
|
|
2990
|
+
list.replaceChildren();
|
|
2991
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
2992
|
+
for (const item of options.items) {
|
|
2993
|
+
const trigger = el("button", cx(px("tabs-trigger"), item.value === value && px("tabs-trigger-active")));
|
|
2994
|
+
trigger.type = "button";
|
|
2995
|
+
trigger.setAttribute("role", "tab");
|
|
2996
|
+
if (item.icon) trigger.appendChild(Icon(item.icon, 14, { className: px("icon") }));
|
|
2997
|
+
const label = document.createElement("span");
|
|
2998
|
+
label.textContent = item.label;
|
|
2999
|
+
trigger.appendChild(label);
|
|
3000
|
+
const listener = () => {
|
|
3001
|
+
value = item.value;
|
|
3002
|
+
renderTriggers();
|
|
3003
|
+
renderPanel();
|
|
3004
|
+
onChange?.(value);
|
|
3005
|
+
};
|
|
3006
|
+
trigger.addEventListener("click", listener);
|
|
3007
|
+
cleanupListeners.push(() => trigger.removeEventListener("click", listener));
|
|
3008
|
+
list.appendChild(trigger);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
renderTriggers();
|
|
3012
|
+
renderPanel();
|
|
3013
|
+
root.append(list, panel);
|
|
3014
|
+
const api = {
|
|
3015
|
+
getValue() {
|
|
3016
|
+
return value;
|
|
3017
|
+
},
|
|
3018
|
+
setValue(newValue) {
|
|
3019
|
+
value = newValue;
|
|
3020
|
+
renderTriggers();
|
|
3021
|
+
renderPanel();
|
|
3022
|
+
},
|
|
3023
|
+
destroy() {
|
|
3024
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
3025
|
+
root.remove();
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
return attachController(root, api);
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
// src/components/overlay/Dialog.ts
|
|
3032
|
+
var dialogCss = `
|
|
3033
|
+
.linteau-dialog-overlay { position: fixed; inset: 0; background: color-mix(in oklch, black 60%, transparent); display: flex; align-items: center; justify-content: center; z-index: 1000; backdrop-filter: blur(4px) }
|
|
3034
|
+
.linteau-dialog-panel { width: min(480px, 92vw); max-height: 86vh; display: flex; flex-direction: column; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-lg); box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); overflow: hidden }
|
|
3035
|
+
.linteau-dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--linteau-border) }
|
|
3036
|
+
.linteau-dialog-title { font-size: 14px; font-weight: 600 }
|
|
3037
|
+
.linteau-dialog-close { background: transparent; border: none; color: var(--linteau-muted-foreground); cursor: pointer; display: flex; padding: 4px; border-radius: var(--linteau-radius-sm) }
|
|
3038
|
+
.linteau-dialog-close:hover { background: var(--linteau-accent); color: var(--linteau-foreground) }
|
|
3039
|
+
.linteau-dialog-body { padding: 16px; overflow-y: auto }
|
|
3040
|
+
.linteau-dialog-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--linteau-border) }
|
|
3041
|
+
`;
|
|
3042
|
+
function Dialog(options = {}) {
|
|
3043
|
+
assertDom("Dialog");
|
|
3044
|
+
ensureComponentStyles("dialog", dialogCss);
|
|
3045
|
+
const closeOnBackdrop = options.closeOnBackdrop ?? true;
|
|
3046
|
+
const closeOnEscape = options.closeOnEscape ?? true;
|
|
3047
|
+
let isOpen = false;
|
|
3048
|
+
let unlockScroll = null;
|
|
3049
|
+
let unbindEscape = null;
|
|
3050
|
+
const overlay = el("div", cx(px("dialog-overlay"), px("hidden")));
|
|
3051
|
+
const panel = el("div", px("dialog-panel"));
|
|
3052
|
+
panel.setAttribute("role", "dialog");
|
|
3053
|
+
panel.setAttribute("aria-modal", "true");
|
|
3054
|
+
const header = el("div", px("dialog-header"));
|
|
3055
|
+
const titleEl = el("div", px("dialog-title"));
|
|
3056
|
+
titleEl.textContent = options.title ?? "";
|
|
3057
|
+
const closeButton = el("button", px("dialog-close"));
|
|
3058
|
+
closeButton.type = "button";
|
|
3059
|
+
const closeIcon = Icon(icons.x, 16);
|
|
3060
|
+
if (closeIcon) closeButton.appendChild(closeIcon);
|
|
3061
|
+
header.append(titleEl, closeButton);
|
|
3062
|
+
const body = el("div", px("dialog-body"));
|
|
3063
|
+
if (options.content) fillContent(body, options.content);
|
|
3064
|
+
const footer = el("div", px("dialog-footer"));
|
|
3065
|
+
if (options.actions?.length) footer.append(...options.actions);
|
|
3066
|
+
panel.append(header, body);
|
|
3067
|
+
if (options.actions?.length) panel.appendChild(footer);
|
|
3068
|
+
overlay.appendChild(panel);
|
|
3069
|
+
function setOpen(value) {
|
|
3070
|
+
isOpen = value;
|
|
3071
|
+
overlay.classList.toggle(px("hidden"), !value);
|
|
3072
|
+
if (value) {
|
|
3073
|
+
unlockScroll = lockBodyScroll();
|
|
3074
|
+
if (closeOnEscape) unbindEscape = onEscapeKey(close);
|
|
3075
|
+
} else {
|
|
3076
|
+
unlockScroll?.();
|
|
3077
|
+
unlockScroll = null;
|
|
3078
|
+
unbindEscape?.();
|
|
3079
|
+
unbindEscape = null;
|
|
3080
|
+
}
|
|
3081
|
+
options.onOpenChange?.(value);
|
|
3082
|
+
}
|
|
3083
|
+
function close() {
|
|
3084
|
+
if (isOpen) setOpen(false);
|
|
3085
|
+
}
|
|
3086
|
+
const backdropListener = (event) => {
|
|
3087
|
+
if (closeOnBackdrop && event.target === overlay) close();
|
|
3088
|
+
};
|
|
3089
|
+
overlay.addEventListener("mousedown", backdropListener);
|
|
3090
|
+
closeButton.addEventListener("click", close);
|
|
3091
|
+
const api = {
|
|
3092
|
+
body,
|
|
3093
|
+
open() {
|
|
3094
|
+
setOpen(true);
|
|
3095
|
+
},
|
|
3096
|
+
close,
|
|
3097
|
+
isOpen() {
|
|
3098
|
+
return isOpen;
|
|
3099
|
+
},
|
|
3100
|
+
setTitle(title) {
|
|
3101
|
+
titleEl.textContent = title;
|
|
3102
|
+
},
|
|
3103
|
+
setContent(content) {
|
|
3104
|
+
fillContent(body, content);
|
|
3105
|
+
},
|
|
3106
|
+
destroy() {
|
|
3107
|
+
overlay.removeEventListener("mousedown", backdropListener);
|
|
3108
|
+
closeButton.removeEventListener("click", close);
|
|
3109
|
+
unlockScroll?.();
|
|
3110
|
+
unbindEscape?.();
|
|
3111
|
+
overlay.remove();
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
return attachController(overlay, api);
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
// src/components/overlay/AlertDialog.ts
|
|
3118
|
+
function AlertDialog(options) {
|
|
3119
|
+
assertDom("AlertDialog");
|
|
3120
|
+
let resolved = false;
|
|
3121
|
+
const cancelButton = Button(options.cancelText ?? "Cancel", {
|
|
3122
|
+
variant: "outline",
|
|
3123
|
+
onClick: () => {
|
|
3124
|
+
resolved = true;
|
|
3125
|
+
options.onCancel?.();
|
|
3126
|
+
dialog.close();
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
3129
|
+
const confirmButton = Button(options.confirmText ?? "Continue", {
|
|
3130
|
+
variant: options.destructive ? "destructive" : "default",
|
|
3131
|
+
onClick: () => {
|
|
3132
|
+
resolved = true;
|
|
3133
|
+
options.onConfirm?.();
|
|
3134
|
+
dialog.close();
|
|
3135
|
+
}
|
|
3136
|
+
});
|
|
3137
|
+
const dialog = Dialog({
|
|
3138
|
+
title: options.title,
|
|
3139
|
+
...options.description !== void 0 ? { content: options.description } : {},
|
|
3140
|
+
actions: [cancelButton, confirmButton],
|
|
3141
|
+
closeOnBackdrop: false,
|
|
3142
|
+
closeOnEscape: true,
|
|
3143
|
+
onOpenChange: (open) => {
|
|
3144
|
+
if (!open && !resolved) options.onCancel?.();
|
|
3145
|
+
resolved = false;
|
|
3146
|
+
}
|
|
3147
|
+
});
|
|
3148
|
+
document.body.appendChild(dialog);
|
|
3149
|
+
return {
|
|
3150
|
+
open() {
|
|
3151
|
+
dialog.open();
|
|
3152
|
+
},
|
|
3153
|
+
close() {
|
|
3154
|
+
dialog.close();
|
|
3155
|
+
},
|
|
3156
|
+
isOpen() {
|
|
3157
|
+
return dialog.isOpen();
|
|
3158
|
+
},
|
|
3159
|
+
destroy() {
|
|
3160
|
+
dialog.destroy();
|
|
3161
|
+
}
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
|
|
3165
|
+
// src/components/overlay/DropdownMenu.ts
|
|
3166
|
+
function isSeparator(entry) {
|
|
3167
|
+
return "separator" in entry;
|
|
3168
|
+
}
|
|
3169
|
+
function renderMenuEntries(container, entries, onSelect, onAfterSelect) {
|
|
3170
|
+
container.replaceChildren();
|
|
3171
|
+
for (const entry of entries) {
|
|
3172
|
+
if (isSeparator(entry)) {
|
|
3173
|
+
container.appendChild(el("div", px("menu-separator")));
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
const item = el("div", cx(px("menu-item"), entry.disabled && px("menu-item-disabled"), entry.destructive && px("menu-item-destructive")));
|
|
3177
|
+
if (entry.icon) item.appendChild(Icon(entry.icon, 14, { className: px("icon") }));
|
|
3178
|
+
const label = document.createElement("span");
|
|
3179
|
+
label.textContent = entry.label;
|
|
3180
|
+
item.appendChild(label);
|
|
3181
|
+
item.addEventListener("click", () => {
|
|
3182
|
+
if (entry.disabled) return;
|
|
3183
|
+
onSelect(entry.value);
|
|
3184
|
+
onAfterSelect();
|
|
3185
|
+
});
|
|
3186
|
+
container.appendChild(item);
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
var menuCss = `
|
|
3190
|
+
.linteau-menu { position: fixed; z-index: 1200; min-width: 160px; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-md); padding: 4px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4) }
|
|
3191
|
+
.linteau-menu-item { display: flex; align-items: center; gap: 8px; padding: 7px 8px; border-radius: var(--linteau-radius-sm); font-size: 12px; color: var(--linteau-foreground); cursor: pointer }
|
|
3192
|
+
.linteau-menu-item:hover:not(.linteau-menu-item-disabled) { background: var(--linteau-accent) }
|
|
3193
|
+
.linteau-menu-item-disabled { opacity: 0.5; cursor: not-allowed }
|
|
3194
|
+
.linteau-menu-item-destructive { color: var(--linteau-destructive) }
|
|
3195
|
+
.linteau-menu-separator { height: 1px; background: var(--linteau-border); margin: 4px 0 }
|
|
3196
|
+
`;
|
|
3197
|
+
function DropdownMenu(options) {
|
|
3198
|
+
assertDom("DropdownMenu");
|
|
3199
|
+
ensureComponentStyles("menu", menuCss);
|
|
3200
|
+
const { anchor, placement = "bottom" } = options;
|
|
3201
|
+
let items = options.items;
|
|
3202
|
+
let isOpen = false;
|
|
3203
|
+
let unbindOutside = null;
|
|
3204
|
+
let unbindEscape = null;
|
|
3205
|
+
const menu2 = el("div", cx(px("menu"), px("hidden")));
|
|
3206
|
+
renderMenuEntries(menu2, items, (value) => options.onSelect?.(value), close);
|
|
3207
|
+
function open() {
|
|
3208
|
+
if (isOpen) return;
|
|
3209
|
+
isOpen = true;
|
|
3210
|
+
document.body.appendChild(menu2);
|
|
3211
|
+
menu2.classList.remove(px("hidden"));
|
|
3212
|
+
positionFloating(anchor, menu2, placement);
|
|
3213
|
+
unbindOutside = onClickOutside([anchor, menu2], close);
|
|
3214
|
+
unbindEscape = onEscapeKey(close);
|
|
3215
|
+
}
|
|
3216
|
+
function close() {
|
|
3217
|
+
if (!isOpen) return;
|
|
3218
|
+
isOpen = false;
|
|
3219
|
+
menu2.classList.add(px("hidden"));
|
|
3220
|
+
menu2.remove();
|
|
3221
|
+
unbindOutside?.();
|
|
3222
|
+
unbindOutside = null;
|
|
3223
|
+
unbindEscape?.();
|
|
3224
|
+
unbindEscape = null;
|
|
3225
|
+
}
|
|
3226
|
+
function toggle() {
|
|
3227
|
+
if (isOpen) close();
|
|
3228
|
+
else open();
|
|
3229
|
+
}
|
|
3230
|
+
const anchorListener = () => toggle();
|
|
3231
|
+
anchor.addEventListener("click", anchorListener);
|
|
3232
|
+
const api = {
|
|
3233
|
+
open,
|
|
3234
|
+
close,
|
|
3235
|
+
toggle,
|
|
3236
|
+
isOpen() {
|
|
3237
|
+
return isOpen;
|
|
3238
|
+
},
|
|
3239
|
+
setItems(newItems) {
|
|
3240
|
+
items = newItems;
|
|
3241
|
+
renderMenuEntries(menu2, items, (value) => options.onSelect?.(value), close);
|
|
3242
|
+
},
|
|
3243
|
+
destroy() {
|
|
3244
|
+
anchor.removeEventListener("click", anchorListener);
|
|
3245
|
+
unbindOutside?.();
|
|
3246
|
+
unbindEscape?.();
|
|
3247
|
+
menu2.remove();
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
return attachController(menu2, api);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
// src/components/overlay/ContextMenu.ts
|
|
3254
|
+
function ContextMenu(options) {
|
|
3255
|
+
assertDom("ContextMenu");
|
|
3256
|
+
ensureComponentStyles("menu", menuCss);
|
|
3257
|
+
const { target } = options;
|
|
3258
|
+
let unbindOutside = null;
|
|
3259
|
+
let unbindEscape = null;
|
|
3260
|
+
const menu2 = el("div", px("menu"));
|
|
3261
|
+
renderMenuEntries(menu2, options.items, (value) => options.onSelect?.(value), close);
|
|
3262
|
+
menu2.style.display = "none";
|
|
3263
|
+
function close() {
|
|
3264
|
+
menu2.style.display = "none";
|
|
3265
|
+
menu2.remove();
|
|
3266
|
+
unbindOutside?.();
|
|
3267
|
+
unbindOutside = null;
|
|
3268
|
+
unbindEscape?.();
|
|
3269
|
+
unbindEscape = null;
|
|
3270
|
+
}
|
|
3271
|
+
const contextListener = (event) => {
|
|
3272
|
+
event.preventDefault();
|
|
3273
|
+
document.body.appendChild(menu2);
|
|
3274
|
+
menu2.style.display = "block";
|
|
3275
|
+
menu2.style.position = "fixed";
|
|
3276
|
+
menu2.style.top = `${event.clientY}px`;
|
|
3277
|
+
menu2.style.left = `${event.clientX}px`;
|
|
3278
|
+
unbindOutside = onClickOutside(menu2, close);
|
|
3279
|
+
unbindEscape = onEscapeKey(close);
|
|
3280
|
+
};
|
|
3281
|
+
target.addEventListener("contextmenu", contextListener);
|
|
3282
|
+
return {
|
|
3283
|
+
destroy() {
|
|
3284
|
+
target.removeEventListener("contextmenu", contextListener);
|
|
3285
|
+
unbindOutside?.();
|
|
3286
|
+
unbindEscape?.();
|
|
3287
|
+
menu2.remove();
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// src/components/overlay/Drawer.ts
|
|
3293
|
+
var drawerCss = `
|
|
3294
|
+
.linteau-drawer-overlay { position: fixed; inset: 0; background: color-mix(in oklch, black 55%, transparent); z-index: 1000 }
|
|
3295
|
+
.linteau-drawer-panel { position: fixed; background: var(--linteau-card); border: 1px solid var(--linteau-border); display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); transition: transform 200ms ease }
|
|
3296
|
+
.linteau-drawer-right { top: 0; right: 0; bottom: 0; width: min(360px, 90vw); transform: translateX(0) }
|
|
3297
|
+
.linteau-drawer-left { top: 0; left: 0; bottom: 0; width: min(360px, 90vw); transform: translateX(0) }
|
|
3298
|
+
.linteau-drawer-top { top: 0; left: 0; right: 0; height: min(320px, 80vh); transform: translateY(0) }
|
|
3299
|
+
.linteau-drawer-bottom { bottom: 0; left: 0; right: 0; height: min(320px, 80vh); transform: translateY(0) }
|
|
3300
|
+
.linteau-drawer-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid var(--linteau-border) }
|
|
3301
|
+
.linteau-drawer-title { font-size: 14px; font-weight: 600 }
|
|
3302
|
+
.linteau-drawer-close { background: transparent; border: none; color: var(--linteau-muted-foreground); cursor: pointer; display: flex; padding: 4px; border-radius: var(--linteau-radius-sm) }
|
|
3303
|
+
.linteau-drawer-body { padding: 16px; overflow-y: auto; flex: 1 }
|
|
3304
|
+
`;
|
|
3305
|
+
function Drawer(options = {}) {
|
|
3306
|
+
assertDom("Drawer");
|
|
3307
|
+
ensureComponentStyles("drawer", drawerCss);
|
|
3308
|
+
const side = options.side ?? "right";
|
|
3309
|
+
const closeOnBackdrop = options.closeOnBackdrop ?? true;
|
|
3310
|
+
const closeOnEscape = options.closeOnEscape ?? true;
|
|
3311
|
+
let isOpen = false;
|
|
3312
|
+
let unlockScroll = null;
|
|
3313
|
+
let unbindEscape = null;
|
|
3314
|
+
const overlay = el("div", cx(px("drawer-overlay"), px("hidden")));
|
|
3315
|
+
const panel = el("div", cx(px("drawer-panel"), px(`drawer-${side}`)));
|
|
3316
|
+
panel.setAttribute("role", "dialog");
|
|
3317
|
+
panel.setAttribute("aria-modal", "true");
|
|
3318
|
+
const header = el("div", px("drawer-header"));
|
|
3319
|
+
const titleEl = el("div", px("drawer-title"));
|
|
3320
|
+
titleEl.textContent = options.title ?? "";
|
|
3321
|
+
const closeButton = el("button", px("drawer-close"));
|
|
3322
|
+
closeButton.type = "button";
|
|
3323
|
+
const closeIcon = Icon(icons.x, 16);
|
|
3324
|
+
if (closeIcon) closeButton.appendChild(closeIcon);
|
|
3325
|
+
header.append(titleEl, closeButton);
|
|
3326
|
+
const body = el("div", px("drawer-body"));
|
|
3327
|
+
if (options.content) fillContent(body, options.content);
|
|
3328
|
+
panel.append(header, body);
|
|
3329
|
+
overlay.appendChild(panel);
|
|
3330
|
+
function setOpen(value) {
|
|
3331
|
+
isOpen = value;
|
|
3332
|
+
overlay.classList.toggle(px("hidden"), !value);
|
|
3333
|
+
if (value) {
|
|
3334
|
+
unlockScroll = lockBodyScroll();
|
|
3335
|
+
if (closeOnEscape) unbindEscape = onEscapeKey(close);
|
|
3336
|
+
} else {
|
|
3337
|
+
unlockScroll?.();
|
|
3338
|
+
unlockScroll = null;
|
|
3339
|
+
unbindEscape?.();
|
|
3340
|
+
unbindEscape = null;
|
|
3341
|
+
}
|
|
3342
|
+
options.onOpenChange?.(value);
|
|
3343
|
+
}
|
|
3344
|
+
function close() {
|
|
3345
|
+
if (isOpen) setOpen(false);
|
|
3346
|
+
}
|
|
3347
|
+
const backdropListener = (event) => {
|
|
3348
|
+
if (closeOnBackdrop && event.target === overlay) close();
|
|
3349
|
+
};
|
|
3350
|
+
overlay.addEventListener("mousedown", backdropListener);
|
|
3351
|
+
closeButton.addEventListener("click", close);
|
|
3352
|
+
const api = {
|
|
3353
|
+
body,
|
|
3354
|
+
open() {
|
|
3355
|
+
setOpen(true);
|
|
3356
|
+
},
|
|
3357
|
+
close,
|
|
3358
|
+
isOpen() {
|
|
3359
|
+
return isOpen;
|
|
3360
|
+
},
|
|
3361
|
+
setTitle(title) {
|
|
3362
|
+
titleEl.textContent = title;
|
|
3363
|
+
},
|
|
3364
|
+
setContent(content) {
|
|
3365
|
+
fillContent(body, content);
|
|
3366
|
+
},
|
|
3367
|
+
destroy() {
|
|
3368
|
+
overlay.removeEventListener("mousedown", backdropListener);
|
|
3369
|
+
closeButton.removeEventListener("click", close);
|
|
3370
|
+
unlockScroll?.();
|
|
3371
|
+
unbindEscape?.();
|
|
3372
|
+
overlay.remove();
|
|
3373
|
+
}
|
|
3374
|
+
};
|
|
3375
|
+
return attachController(overlay, api);
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
// src/components/overlay/Popover.ts
|
|
3379
|
+
var popoverCss = `
|
|
3380
|
+
.linteau-popover { position: fixed; z-index: 1200; background: var(--linteau-card); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-md); padding: 10px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4) }
|
|
3381
|
+
`;
|
|
3382
|
+
function Popover(options) {
|
|
3383
|
+
assertDom("Popover");
|
|
3384
|
+
ensureComponentStyles("popover", popoverCss);
|
|
3385
|
+
const { anchor, placement = "bottom", closeOnClickOutside = true } = options;
|
|
3386
|
+
let isOpen = false;
|
|
3387
|
+
let unbindOutside = null;
|
|
3388
|
+
let unbindEscape = null;
|
|
3389
|
+
const panel = el("div", cx(px("popover"), px("hidden")));
|
|
3390
|
+
fillContent(panel, options.content);
|
|
3391
|
+
function open() {
|
|
3392
|
+
if (isOpen) return;
|
|
3393
|
+
isOpen = true;
|
|
3394
|
+
document.body.appendChild(panel);
|
|
3395
|
+
panel.classList.remove(px("hidden"));
|
|
3396
|
+
positionFloating(anchor, panel, placement);
|
|
3397
|
+
if (closeOnClickOutside) unbindOutside = onClickOutside([anchor, panel], close);
|
|
3398
|
+
unbindEscape = onEscapeKey(close);
|
|
3399
|
+
}
|
|
3400
|
+
function close() {
|
|
3401
|
+
if (!isOpen) return;
|
|
3402
|
+
isOpen = false;
|
|
3403
|
+
panel.classList.add(px("hidden"));
|
|
3404
|
+
panel.remove();
|
|
3405
|
+
unbindOutside?.();
|
|
3406
|
+
unbindOutside = null;
|
|
3407
|
+
unbindEscape?.();
|
|
3408
|
+
unbindEscape = null;
|
|
3409
|
+
}
|
|
3410
|
+
function toggle() {
|
|
3411
|
+
if (isOpen) close();
|
|
3412
|
+
else open();
|
|
3413
|
+
}
|
|
3414
|
+
const anchorListener = () => toggle();
|
|
3415
|
+
anchor.addEventListener("click", anchorListener);
|
|
3416
|
+
const api = {
|
|
3417
|
+
open,
|
|
3418
|
+
close,
|
|
3419
|
+
toggle,
|
|
3420
|
+
isOpen() {
|
|
3421
|
+
return isOpen;
|
|
3422
|
+
},
|
|
3423
|
+
setContent(content) {
|
|
3424
|
+
fillContent(panel, content);
|
|
3425
|
+
},
|
|
3426
|
+
destroy() {
|
|
3427
|
+
anchor.removeEventListener("click", anchorListener);
|
|
3428
|
+
unbindOutside?.();
|
|
3429
|
+
unbindEscape?.();
|
|
3430
|
+
panel.remove();
|
|
3431
|
+
}
|
|
3432
|
+
};
|
|
3433
|
+
return attachController(panel, api);
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
// src/components/radio/RadioGroup.ts
|
|
3437
|
+
var radioCss = `
|
|
3438
|
+
.linteau-radio-group { display: flex; flex-direction: column; gap: 8px }
|
|
3439
|
+
.linteau-radio-row { display: flex; align-items: center; gap: 8px; cursor: pointer }
|
|
3440
|
+
.linteau-radio-row.linteau-disabled { opacity: 0.5; cursor: not-allowed }
|
|
3441
|
+
.linteau-radio { width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--linteau-border); background: var(--linteau-secondary); display: flex; align-items: center; justify-content: center; flex-shrink: 0 }
|
|
3442
|
+
.linteau-radio.linteau-checked { border-color: var(--linteau-primary) }
|
|
3443
|
+
.linteau-radio-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--linteau-primary); opacity: 0; transition: opacity 100ms ease }
|
|
3444
|
+
.linteau-radio.linteau-checked .linteau-radio-dot { opacity: 1 }
|
|
3445
|
+
`;
|
|
3446
|
+
function RadioGroup(options) {
|
|
3447
|
+
assertDom("RadioGroup");
|
|
3448
|
+
ensureComponentStyles("radio", radioCss);
|
|
3449
|
+
let value = options.value ?? options.defaultValue;
|
|
3450
|
+
const groupDisabled = options.disabled ?? false;
|
|
3451
|
+
const onChange = options.onChange;
|
|
3452
|
+
const cleanupListeners = [];
|
|
3453
|
+
const root = el("div", px("radio-group"));
|
|
3454
|
+
root.setAttribute("role", "radiogroup");
|
|
3455
|
+
function render() {
|
|
3456
|
+
root.replaceChildren();
|
|
3457
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
3458
|
+
for (const item of options.items) {
|
|
3459
|
+
const itemDisabled = groupDisabled || (item.disabled ?? false);
|
|
3460
|
+
const row = el("div", cx(px("radio-row"), itemDisabled && px("disabled")));
|
|
3461
|
+
const dot = el("div", cx(px("radio"), item.value === value && px("checked")));
|
|
3462
|
+
dot.appendChild(el("div", px("radio-dot")));
|
|
3463
|
+
const label = document.createElement("span");
|
|
3464
|
+
label.textContent = item.label;
|
|
3465
|
+
row.append(dot, label);
|
|
3466
|
+
const listener = () => {
|
|
3467
|
+
if (itemDisabled) return;
|
|
3468
|
+
value = item.value;
|
|
3469
|
+
render();
|
|
3470
|
+
onChange?.(item.value);
|
|
3471
|
+
};
|
|
3472
|
+
row.addEventListener("click", listener);
|
|
3473
|
+
cleanupListeners.push(() => row.removeEventListener("click", listener));
|
|
3474
|
+
root.appendChild(row);
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
render();
|
|
3478
|
+
const api = {
|
|
3479
|
+
getValue() {
|
|
3480
|
+
return value;
|
|
3481
|
+
},
|
|
3482
|
+
setValue(newValue) {
|
|
3483
|
+
value = newValue;
|
|
3484
|
+
render();
|
|
3485
|
+
},
|
|
3486
|
+
destroy() {
|
|
3487
|
+
cleanupListeners.splice(0).forEach((fn) => fn());
|
|
3488
|
+
root.remove();
|
|
3489
|
+
}
|
|
3490
|
+
};
|
|
3491
|
+
return attachController(root, api);
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
// src/components/select/Select.ts
|
|
3495
|
+
var selectCss = `
|
|
3496
|
+
.linteau-select-wrap { position: relative }
|
|
3497
|
+
.linteau-select { appearance: none; background: var(--linteau-secondary); color: var(--linteau-foreground); border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-sm); padding: 6px 28px 6px 10px; font-size: 12px; font-family: inherit; cursor: pointer; transition: border-color 120ms ease }
|
|
3498
|
+
.linteau-select:disabled { opacity: 0.5; cursor: not-allowed }
|
|
3499
|
+
.linteau-select:focus { outline: none; border-color: var(--linteau-ring); box-shadow: 0 0 0 2px color-mix(in oklch, var(--linteau-ring) 20%, transparent) }
|
|
3500
|
+
.linteau-select-chevron { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); pointer-events: none }
|
|
3501
|
+
`;
|
|
3502
|
+
function Select(options) {
|
|
3503
|
+
assertDom("Select");
|
|
3504
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
3505
|
+
ensureComponentStyles("select", selectCss);
|
|
3506
|
+
let items = options.options;
|
|
3507
|
+
let value = options.value ?? options.defaultValue ?? items[0]?.value ?? "";
|
|
3508
|
+
const onChange = options.onChange;
|
|
3509
|
+
const root = el("div", px("field-row"));
|
|
3510
|
+
const labelWrap = el("div", px("field-label"));
|
|
3511
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
3512
|
+
const labelText = document.createElement("span");
|
|
3513
|
+
labelText.textContent = options.label;
|
|
3514
|
+
labelWrap.appendChild(labelText);
|
|
3515
|
+
const selectWrap = el("div", px("select-wrap"));
|
|
3516
|
+
const select = el("select", px("select"));
|
|
3517
|
+
select.disabled = options.disabled ?? false;
|
|
3518
|
+
function renderOptions() {
|
|
3519
|
+
select.replaceChildren();
|
|
3520
|
+
if (options.placeholder) {
|
|
3521
|
+
const placeholderOption = document.createElement("option");
|
|
3522
|
+
placeholderOption.value = "";
|
|
3523
|
+
placeholderOption.textContent = options.placeholder;
|
|
3524
|
+
placeholderOption.disabled = true;
|
|
3525
|
+
placeholderOption.selected = !value;
|
|
3526
|
+
select.appendChild(placeholderOption);
|
|
3527
|
+
}
|
|
3528
|
+
for (const item of items) {
|
|
3529
|
+
const optionEl = document.createElement("option");
|
|
3530
|
+
optionEl.value = item.value;
|
|
3531
|
+
optionEl.textContent = item.label;
|
|
3532
|
+
optionEl.disabled = item.disabled ?? false;
|
|
3533
|
+
optionEl.selected = item.value === value;
|
|
3534
|
+
select.appendChild(optionEl);
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
renderOptions();
|
|
3538
|
+
const chevron = Icon(icons.chevronDown, 12, { className: px("select-chevron") });
|
|
3539
|
+
selectWrap.append(select, chevron);
|
|
3540
|
+
const listener = () => {
|
|
3541
|
+
value = select.value;
|
|
3542
|
+
onChange?.(value);
|
|
3543
|
+
};
|
|
3544
|
+
select.addEventListener("change", listener);
|
|
3545
|
+
root.append(labelWrap, selectWrap);
|
|
3546
|
+
const api = {
|
|
3547
|
+
getValue() {
|
|
3548
|
+
return value;
|
|
3549
|
+
},
|
|
3550
|
+
setValue(newValue) {
|
|
3551
|
+
value = newValue;
|
|
3552
|
+
select.value = newValue;
|
|
3553
|
+
},
|
|
3554
|
+
setOptions(newOptions) {
|
|
3555
|
+
items = newOptions;
|
|
3556
|
+
renderOptions();
|
|
3557
|
+
},
|
|
3558
|
+
setDisabled(disabled) {
|
|
3559
|
+
select.disabled = disabled;
|
|
3560
|
+
},
|
|
3561
|
+
destroy() {
|
|
3562
|
+
select.removeEventListener("change", listener);
|
|
3563
|
+
root.remove();
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
return attachController(root, api);
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// src/components/slider/Slider.ts
|
|
3570
|
+
var sliderCss = `
|
|
3571
|
+
.linteau-slider { -webkit-appearance: none; appearance: none; width: 100%; height: 6px; border-radius: 999px; background: linear-gradient(to right, var(--linteau-primary) 0%, var(--linteau-primary) var(--linteau-fill, 0%), var(--linteau-secondary) var(--linteau-fill, 0%), var(--linteau-secondary) 100%); outline: none; transition: background 120ms ease }
|
|
3572
|
+
.linteau-slider:disabled { opacity: 0.5 }
|
|
3573
|
+
.linteau-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--linteau-primary); border: 2px solid var(--linteau-card); box-shadow: 0 0 0 1px var(--linteau-border); cursor: pointer; transition: transform 120ms ease }
|
|
3574
|
+
.linteau-slider::-webkit-slider-thumb:hover { transform: scale(1.15) }
|
|
3575
|
+
.linteau-slider::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--linteau-primary); border: 2px solid var(--linteau-card); cursor: pointer }
|
|
3576
|
+
`;
|
|
3577
|
+
function Slider(options) {
|
|
3578
|
+
assertDom("Slider");
|
|
3579
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
3580
|
+
ensureComponentStyles("slider", sliderCss);
|
|
3581
|
+
const { min, max, step } = options;
|
|
3582
|
+
let value = options.value ?? options.defaultValue ?? min;
|
|
3583
|
+
const format = options.format ?? ((v) => v.toString());
|
|
3584
|
+
const onChange = options.onChange;
|
|
3585
|
+
const root = el("div", px("field"));
|
|
3586
|
+
const header = el("div", px("field-header"));
|
|
3587
|
+
const labelWrap = el("div", px("field-label"));
|
|
3588
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
3589
|
+
const labelText = document.createElement("span");
|
|
3590
|
+
labelText.textContent = options.label;
|
|
3591
|
+
labelWrap.appendChild(labelText);
|
|
3592
|
+
const valueEl = el("span", px("field-value"));
|
|
3593
|
+
valueEl.textContent = format(value);
|
|
3594
|
+
header.append(labelWrap, valueEl);
|
|
3595
|
+
const input = el("input", px("slider"));
|
|
3596
|
+
input.type = "range";
|
|
3597
|
+
input.min = String(min);
|
|
3598
|
+
input.max = String(max);
|
|
3599
|
+
input.step = String(step);
|
|
3600
|
+
input.value = String(value);
|
|
3601
|
+
input.disabled = options.disabled ?? false;
|
|
3602
|
+
function updateFill(v) {
|
|
3603
|
+
const percent = (v - min) / (max - min) * 100;
|
|
3604
|
+
input.style.setProperty("--linteau-fill", `${percent}%`);
|
|
3605
|
+
}
|
|
3606
|
+
updateFill(value);
|
|
3607
|
+
const listener = () => {
|
|
3608
|
+
value = Number(input.value);
|
|
3609
|
+
valueEl.textContent = format(value);
|
|
3610
|
+
updateFill(value);
|
|
3611
|
+
onChange?.(value);
|
|
3612
|
+
};
|
|
3613
|
+
input.addEventListener("input", listener);
|
|
3614
|
+
root.append(header, input);
|
|
3615
|
+
const api = {
|
|
3616
|
+
getValue() {
|
|
3617
|
+
return value;
|
|
3618
|
+
},
|
|
3619
|
+
setValue(newValue) {
|
|
3620
|
+
value = Math.max(min, Math.min(max, newValue));
|
|
3621
|
+
input.value = String(value);
|
|
3622
|
+
valueEl.textContent = format(value);
|
|
3623
|
+
updateFill(value);
|
|
3624
|
+
},
|
|
3625
|
+
setDisabled(disabled) {
|
|
3626
|
+
input.disabled = disabled;
|
|
3627
|
+
},
|
|
3628
|
+
destroy() {
|
|
3629
|
+
input.removeEventListener("input", listener);
|
|
3630
|
+
root.remove();
|
|
3631
|
+
}
|
|
3632
|
+
};
|
|
3633
|
+
return attachController(root, api);
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// src/components/slider/RangeSlider.ts
|
|
3637
|
+
var rangeSliderCss = `
|
|
3638
|
+
.linteau-range-track { position: relative; height: 20px; display: flex; align-items: center }
|
|
3639
|
+
.linteau-range-track::before { content: ''; position: absolute; left: 0; right: 0; height: 6px; border-radius: 999px; background: var(--linteau-secondary) }
|
|
3640
|
+
.linteau-range-track::after { content: ''; position: absolute; height: 6px; border-radius: 999px; background: var(--linteau-primary); left: var(--linteau-range-low, 0%); right: calc(100% - var(--linteau-range-high, 100%)) }
|
|
3641
|
+
.linteau-range-input { position: absolute; left: 0; right: 0; width: 100%; height: 20px; background: transparent; pointer-events: none; margin: 0 }
|
|
3642
|
+
.linteau-range-input::-webkit-slider-thumb { pointer-events: auto }
|
|
3643
|
+
.linteau-range-input::-moz-range-thumb { pointer-events: auto }
|
|
3644
|
+
`;
|
|
3645
|
+
function RangeSlider(options) {
|
|
3646
|
+
assertDom("RangeSlider");
|
|
3647
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
3648
|
+
ensureComponentStyles("slider", sliderCss);
|
|
3649
|
+
ensureComponentStyles("range-slider", rangeSliderCss);
|
|
3650
|
+
const { min, max, step } = options;
|
|
3651
|
+
let [low, high] = options.value ?? options.defaultValue ?? [min, max];
|
|
3652
|
+
const format = options.format ?? ((v) => v.toString());
|
|
3653
|
+
const onChange = options.onChange;
|
|
3654
|
+
const root = el("div", px("field"));
|
|
3655
|
+
const header = el("div", px("field-header"));
|
|
3656
|
+
const labelWrap = el("div", px("field-label"));
|
|
3657
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
3658
|
+
const labelText = document.createElement("span");
|
|
3659
|
+
labelText.textContent = options.label;
|
|
3660
|
+
labelWrap.appendChild(labelText);
|
|
3661
|
+
const valueEl = el("span", px("field-value"));
|
|
3662
|
+
header.append(labelWrap, valueEl);
|
|
3663
|
+
const track = el("div", px("range-track"));
|
|
3664
|
+
const inputLow = el("input", cx(px("slider"), px("range-input")));
|
|
3665
|
+
const inputHigh = el("input", cx(px("slider"), px("range-input")));
|
|
3666
|
+
for (const input of [inputLow, inputHigh]) {
|
|
3667
|
+
input.type = "range";
|
|
3668
|
+
input.min = String(min);
|
|
3669
|
+
input.max = String(max);
|
|
3670
|
+
input.step = String(step);
|
|
3671
|
+
input.disabled = options.disabled ?? false;
|
|
3672
|
+
}
|
|
3673
|
+
function syncDisplay() {
|
|
3674
|
+
valueEl.textContent = `${format(low)} - ${format(high)}`;
|
|
3675
|
+
inputLow.value = String(low);
|
|
3676
|
+
inputHigh.value = String(high);
|
|
3677
|
+
track.style.setProperty("--linteau-range-low", `${(low - min) / (max - min) * 100}%`);
|
|
3678
|
+
track.style.setProperty("--linteau-range-high", `${(high - min) / (max - min) * 100}%`);
|
|
3679
|
+
}
|
|
3680
|
+
syncDisplay();
|
|
3681
|
+
const lowListener = () => {
|
|
3682
|
+
low = Math.min(Number(inputLow.value), high);
|
|
3683
|
+
syncDisplay();
|
|
3684
|
+
onChange?.([low, high]);
|
|
3685
|
+
};
|
|
3686
|
+
const highListener = () => {
|
|
3687
|
+
high = Math.max(Number(inputHigh.value), low);
|
|
3688
|
+
syncDisplay();
|
|
3689
|
+
onChange?.([low, high]);
|
|
3690
|
+
};
|
|
3691
|
+
inputLow.addEventListener("input", lowListener);
|
|
3692
|
+
inputHigh.addEventListener("input", highListener);
|
|
3693
|
+
track.append(inputLow, inputHigh);
|
|
3694
|
+
root.append(header, track);
|
|
3695
|
+
const api = {
|
|
3696
|
+
getValue() {
|
|
3697
|
+
return [low, high];
|
|
3698
|
+
},
|
|
3699
|
+
setValue(value) {
|
|
3700
|
+
;
|
|
3701
|
+
[low, high] = value;
|
|
3702
|
+
syncDisplay();
|
|
3703
|
+
},
|
|
3704
|
+
setDisabled(disabled) {
|
|
3705
|
+
inputLow.disabled = disabled;
|
|
3706
|
+
inputHigh.disabled = disabled;
|
|
3707
|
+
},
|
|
3708
|
+
destroy() {
|
|
3709
|
+
inputLow.removeEventListener("input", lowListener);
|
|
3710
|
+
inputHigh.removeEventListener("input", highListener);
|
|
3711
|
+
root.remove();
|
|
3712
|
+
}
|
|
3713
|
+
};
|
|
3714
|
+
return attachController(root, api);
|
|
3715
|
+
}
|
|
3716
|
+
|
|
3717
|
+
// src/components/switch/Switch.ts
|
|
3718
|
+
var switchCss = `
|
|
3719
|
+
.linteau-switch-row { display: flex; align-items: center; justify-content: space-between }
|
|
3720
|
+
.linteau-switch { width: 38px; height: 22px; border-radius: 999px; background: var(--linteau-secondary); border: 1px solid var(--linteau-border); padding: 2px; cursor: pointer; display: flex; align-items: center; transition: background 150ms ease }
|
|
3721
|
+
.linteau-switch:disabled { opacity: 0.5; cursor: not-allowed }
|
|
3722
|
+
.linteau-switch.linteau-checked { background: var(--linteau-primary) }
|
|
3723
|
+
.linteau-switch-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--linteau-card); transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); transform: translateX(0) }
|
|
3724
|
+
.linteau-switch.linteau-checked .linteau-switch-thumb { transform: translateX(16px); background: var(--linteau-primary-foreground) }
|
|
3725
|
+
`;
|
|
3726
|
+
function Switch(options) {
|
|
3727
|
+
assertDom("Switch");
|
|
3728
|
+
ensureComponentStyles("field-base", fieldBaseCss);
|
|
3729
|
+
ensureComponentStyles("switch", switchCss);
|
|
3730
|
+
let checked = options.checked ?? options.defaultChecked ?? false;
|
|
3731
|
+
let disabled = options.disabled ?? false;
|
|
3732
|
+
const onChange = options.onChange;
|
|
3733
|
+
const root = el("div", px("switch-row"));
|
|
3734
|
+
const labelWrap = el("div", px("field-label"));
|
|
3735
|
+
if (options.icon) labelWrap.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
3736
|
+
const labelText = document.createElement("span");
|
|
3737
|
+
labelText.textContent = options.label;
|
|
3738
|
+
labelWrap.appendChild(labelText);
|
|
3739
|
+
const track = el("button", px("switch"));
|
|
3740
|
+
track.type = "button";
|
|
3741
|
+
track.setAttribute("role", "switch");
|
|
3742
|
+
track.disabled = disabled;
|
|
3743
|
+
const thumb = el("span", px("switch-thumb"));
|
|
3744
|
+
track.appendChild(thumb);
|
|
3745
|
+
function applyState() {
|
|
3746
|
+
track.classList.toggle(px("checked"), checked);
|
|
3747
|
+
track.setAttribute("aria-checked", String(checked));
|
|
3748
|
+
}
|
|
3749
|
+
applyState();
|
|
3750
|
+
const listener = () => {
|
|
3751
|
+
if (disabled) return;
|
|
3752
|
+
checked = !checked;
|
|
3753
|
+
applyState();
|
|
3754
|
+
onChange?.(checked);
|
|
3755
|
+
};
|
|
3756
|
+
track.addEventListener("click", listener);
|
|
3757
|
+
root.append(labelWrap, track);
|
|
3758
|
+
const api = {
|
|
3759
|
+
getValue() {
|
|
3760
|
+
return checked;
|
|
3761
|
+
},
|
|
3762
|
+
setValue(value) {
|
|
3763
|
+
checked = value;
|
|
3764
|
+
applyState();
|
|
3765
|
+
},
|
|
3766
|
+
setLabel(newLabel) {
|
|
3767
|
+
labelText.textContent = newLabel;
|
|
3768
|
+
},
|
|
3769
|
+
setDisabled(value) {
|
|
3770
|
+
disabled = value;
|
|
3771
|
+
track.disabled = value;
|
|
3772
|
+
},
|
|
3773
|
+
destroy() {
|
|
3774
|
+
track.removeEventListener("click", listener);
|
|
3775
|
+
root.remove();
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
return attachController(root, api);
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// src/components/toggle/Toggle.ts
|
|
3782
|
+
var toggleCss = `
|
|
3783
|
+
.linteau-toggle {
|
|
3784
|
+
display: inline-flex;
|
|
3785
|
+
align-items: center;
|
|
3786
|
+
justify-content: center;
|
|
3787
|
+
gap: 6px;
|
|
3788
|
+
border-radius: var(--linteau-radius-sm);
|
|
3789
|
+
border: 1px solid transparent;
|
|
3790
|
+
background: transparent;
|
|
3791
|
+
color: var(--linteau-muted-foreground);
|
|
3792
|
+
font-size: 13px;
|
|
3793
|
+
font-weight: 500;
|
|
3794
|
+
cursor: pointer;
|
|
3795
|
+
font-family: inherit;
|
|
3796
|
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
3797
|
+
}
|
|
3798
|
+
.linteau-toggle-default { padding: 8px 12px }
|
|
3799
|
+
.linteau-toggle-sm { padding: 5px 8px; font-size: 12px }
|
|
3800
|
+
.linteau-toggle:hover:not(:disabled) { background: var(--linteau-accent); color: var(--linteau-foreground) }
|
|
3801
|
+
.linteau-toggle:disabled { opacity: 0.5; cursor: not-allowed }
|
|
3802
|
+
.linteau-toggle-outline { border-color: var(--linteau-border) }
|
|
3803
|
+
.linteau-toggle.linteau-pressed { background: var(--linteau-accent); color: var(--linteau-foreground) }
|
|
3804
|
+
`;
|
|
3805
|
+
function Toggle(text, options = {}) {
|
|
3806
|
+
assertDom("Toggle");
|
|
3807
|
+
ensureComponentStyles("toggle", toggleCss);
|
|
3808
|
+
const variant = options.variant ?? "default";
|
|
3809
|
+
const size = options.size ?? "default";
|
|
3810
|
+
let pressed = options.pressed ?? options.defaultPressed ?? false;
|
|
3811
|
+
let disabled = options.disabled ?? false;
|
|
3812
|
+
const onPressedChange = options.onPressedChange;
|
|
3813
|
+
const button = el("button", cx(px("toggle"), px(`toggle-${size}`), variant === "outline" && px("toggle-outline")));
|
|
3814
|
+
button.type = "button";
|
|
3815
|
+
button.setAttribute("aria-pressed", String(pressed));
|
|
3816
|
+
button.disabled = disabled;
|
|
3817
|
+
if (options.icon) button.appendChild(Icon(options.icon, 14, { className: px("icon") }));
|
|
3818
|
+
const label = document.createElement("span");
|
|
3819
|
+
label.textContent = text;
|
|
3820
|
+
button.appendChild(label);
|
|
3821
|
+
function applyState() {
|
|
3822
|
+
button.classList.toggle(px("pressed"), pressed);
|
|
3823
|
+
button.setAttribute("aria-pressed", String(pressed));
|
|
3824
|
+
}
|
|
3825
|
+
applyState();
|
|
3826
|
+
const listener = () => {
|
|
3827
|
+
if (disabled) return;
|
|
3828
|
+
pressed = !pressed;
|
|
3829
|
+
applyState();
|
|
3830
|
+
onPressedChange?.(pressed);
|
|
3831
|
+
};
|
|
3832
|
+
button.addEventListener("click", listener);
|
|
3833
|
+
const api = {
|
|
3834
|
+
getPressed() {
|
|
3835
|
+
return pressed;
|
|
3836
|
+
},
|
|
3837
|
+
setPressed(value) {
|
|
3838
|
+
pressed = value;
|
|
3839
|
+
applyState();
|
|
3840
|
+
},
|
|
3841
|
+
setDisabled(value) {
|
|
3842
|
+
disabled = value;
|
|
3843
|
+
button.disabled = value;
|
|
3844
|
+
},
|
|
3845
|
+
destroy() {
|
|
3846
|
+
button.removeEventListener("click", listener);
|
|
3847
|
+
button.remove();
|
|
3848
|
+
}
|
|
3849
|
+
};
|
|
3850
|
+
return attachController(button, api);
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
// src/components/toggle/ToggleGroup.ts
|
|
3854
|
+
var toggleGroupCss = `
|
|
3855
|
+
.linteau-toggle-group { display: inline-flex; gap: 2px; padding: 2px; border: 1px solid var(--linteau-border); border-radius: var(--linteau-radius-sm) }
|
|
3856
|
+
`;
|
|
3857
|
+
function ToggleGroup(options) {
|
|
3858
|
+
assertDom("ToggleGroup");
|
|
3859
|
+
ensureComponentStyles("toggle-group", toggleGroupCss);
|
|
3860
|
+
const type = options.type ?? "single";
|
|
3861
|
+
const variant = options.variant ?? "outline";
|
|
3862
|
+
const size = options.size ?? "default";
|
|
3863
|
+
const groupDisabled = options.disabled ?? false;
|
|
3864
|
+
const onChange = options.onChange;
|
|
3865
|
+
let selected = new Set(
|
|
3866
|
+
Array.isArray(options.value ?? options.defaultValue) ? options.value ?? options.defaultValue : options.value !== void 0 || options.defaultValue !== void 0 ? [options.value ?? options.defaultValue] : []
|
|
3867
|
+
);
|
|
3868
|
+
const root = el("div", px("toggle-group"));
|
|
3869
|
+
root.setAttribute("role", "group");
|
|
3870
|
+
const toggles = /* @__PURE__ */ new Map();
|
|
3871
|
+
function currentValue() {
|
|
3872
|
+
if (type === "single") return Array.from(selected)[0] ?? "";
|
|
3873
|
+
return Array.from(selected);
|
|
3874
|
+
}
|
|
3875
|
+
function toggleItem(value) {
|
|
3876
|
+
if (type === "single") {
|
|
3877
|
+
selected = selected.has(value) ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([value]);
|
|
3878
|
+
} else {
|
|
3879
|
+
if (selected.has(value)) selected.delete(value);
|
|
3880
|
+
else selected.add(value);
|
|
3881
|
+
}
|
|
3882
|
+
for (const [itemValue, toggle] of toggles) toggle.setPressed(selected.has(itemValue));
|
|
3883
|
+
onChange?.(currentValue());
|
|
3884
|
+
}
|
|
3885
|
+
for (const item of options.items) {
|
|
3886
|
+
const toggle = Toggle(item.label, {
|
|
3887
|
+
variant,
|
|
3888
|
+
size,
|
|
3889
|
+
icon: item.icon,
|
|
3890
|
+
pressed: selected.has(item.value),
|
|
3891
|
+
disabled: groupDisabled || (item.disabled ?? false),
|
|
3892
|
+
onPressedChange: () => toggleItem(item.value)
|
|
3893
|
+
});
|
|
3894
|
+
toggles.set(item.value, toggle);
|
|
3895
|
+
root.appendChild(toggle);
|
|
3896
|
+
}
|
|
3897
|
+
const api = {
|
|
3898
|
+
getValue() {
|
|
3899
|
+
return currentValue();
|
|
3900
|
+
},
|
|
3901
|
+
setValue(value) {
|
|
3902
|
+
selected = new Set(Array.isArray(value) ? value : [value]);
|
|
3903
|
+
for (const [itemValue, toggle] of toggles) toggle.setPressed(selected.has(itemValue));
|
|
3904
|
+
},
|
|
3905
|
+
destroy() {
|
|
3906
|
+
for (const toggle of toggles.values()) toggle.destroy();
|
|
3907
|
+
root.remove();
|
|
3908
|
+
}
|
|
3909
|
+
};
|
|
3910
|
+
return attachController(root, api);
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// src/components/tooltip/Tooltip.ts
|
|
3914
|
+
var tooltipCss = `
|
|
3915
|
+
.linteau-tooltip { position: fixed; z-index: 1300; background: var(--linteau-foreground); color: var(--linteau-background); font-size: 11px; font-weight: 500; padding: 5px 8px; border-radius: var(--linteau-radius-sm); pointer-events: none; box-shadow: 0 6px 20px rgba(0, 0, 0, 0.3) }
|
|
3916
|
+
`;
|
|
3917
|
+
function Tooltip(anchor, content, options = {}) {
|
|
3918
|
+
assertDom("Tooltip");
|
|
3919
|
+
ensureComponentStyles("tooltip", tooltipCss);
|
|
3920
|
+
const placement = options.placement ?? "top";
|
|
3921
|
+
const delay = options.delay ?? 200;
|
|
3922
|
+
let text = content;
|
|
3923
|
+
let showTimer = null;
|
|
3924
|
+
const tooltip = el("div", px("tooltip"));
|
|
3925
|
+
tooltip.textContent = text;
|
|
3926
|
+
tooltip.style.display = "none";
|
|
3927
|
+
function show() {
|
|
3928
|
+
showTimer = setTimeout(() => {
|
|
3929
|
+
document.body.appendChild(tooltip);
|
|
3930
|
+
tooltip.style.display = "block";
|
|
3931
|
+
positionFloating(anchor, tooltip, placement);
|
|
3932
|
+
}, delay);
|
|
3933
|
+
}
|
|
3934
|
+
function hide() {
|
|
3935
|
+
if (showTimer) clearTimeout(showTimer);
|
|
3936
|
+
tooltip.style.display = "none";
|
|
3937
|
+
tooltip.remove();
|
|
3938
|
+
}
|
|
3939
|
+
anchor.addEventListener("mouseenter", show);
|
|
3940
|
+
anchor.addEventListener("mouseleave", hide);
|
|
3941
|
+
anchor.addEventListener("focus", show);
|
|
3942
|
+
anchor.addEventListener("blur", hide);
|
|
3943
|
+
return {
|
|
3944
|
+
setContent(newContent) {
|
|
3945
|
+
text = newContent;
|
|
3946
|
+
tooltip.textContent = text;
|
|
3947
|
+
},
|
|
3948
|
+
destroy() {
|
|
3949
|
+
hide();
|
|
3950
|
+
anchor.removeEventListener("mouseenter", show);
|
|
3951
|
+
anchor.removeEventListener("mouseleave", hide);
|
|
3952
|
+
anchor.removeEventListener("focus", show);
|
|
3953
|
+
anchor.removeEventListener("blur", hide);
|
|
3954
|
+
}
|
|
3955
|
+
};
|
|
3956
|
+
}
|
|
3957
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3958
|
+
0 && (module.exports = {
|
|
3959
|
+
Accordion,
|
|
3960
|
+
Alert,
|
|
3961
|
+
AlertDialog,
|
|
3962
|
+
Avatar,
|
|
3963
|
+
Badge,
|
|
3964
|
+
Breadcrumb,
|
|
3965
|
+
Button,
|
|
3966
|
+
ButtonGroup,
|
|
3967
|
+
Card,
|
|
3968
|
+
Checkbox,
|
|
3969
|
+
ChipInput,
|
|
3970
|
+
ColorField,
|
|
3971
|
+
Combobox,
|
|
3972
|
+
Command,
|
|
3973
|
+
ContextMenu,
|
|
3974
|
+
DatePicker,
|
|
3975
|
+
Dialog,
|
|
3976
|
+
Drawer,
|
|
3977
|
+
DropdownMenu,
|
|
3978
|
+
FileDropzone,
|
|
3979
|
+
HoverCard,
|
|
3980
|
+
Icon,
|
|
3981
|
+
List,
|
|
3982
|
+
ModeCard,
|
|
3983
|
+
NumberField,
|
|
3984
|
+
Pagination,
|
|
3985
|
+
Popover,
|
|
3986
|
+
ProgressBar,
|
|
3987
|
+
RadioGroup,
|
|
3988
|
+
RangeSlider,
|
|
3989
|
+
Section,
|
|
3990
|
+
Select,
|
|
3991
|
+
Separator,
|
|
3992
|
+
Skeleton,
|
|
3993
|
+
Slider,
|
|
3994
|
+
Spinner,
|
|
3995
|
+
Switch,
|
|
3996
|
+
Table,
|
|
3997
|
+
Tabs,
|
|
3998
|
+
TextArea,
|
|
3999
|
+
TextField,
|
|
4000
|
+
Toggle,
|
|
4001
|
+
ToggleGroup,
|
|
4002
|
+
Tooltip,
|
|
4003
|
+
Tree,
|
|
4004
|
+
accordionCss,
|
|
4005
|
+
alertCss,
|
|
4006
|
+
avatarCss,
|
|
4007
|
+
badgeCss,
|
|
4008
|
+
breadcrumbCss,
|
|
4009
|
+
buttonCss,
|
|
4010
|
+
buttonGroupCss,
|
|
4011
|
+
calendarCss,
|
|
4012
|
+
cardCss,
|
|
4013
|
+
checkboxCss,
|
|
4014
|
+
chipInputCss,
|
|
4015
|
+
comboboxCss,
|
|
4016
|
+
commandCss,
|
|
4017
|
+
createToastManager,
|
|
4018
|
+
dialogCss,
|
|
4019
|
+
drawerCss,
|
|
4020
|
+
dropzoneCss,
|
|
4021
|
+
getTheme,
|
|
4022
|
+
hoverCardCss,
|
|
4023
|
+
icons,
|
|
4024
|
+
isBrowser,
|
|
4025
|
+
listCss,
|
|
4026
|
+
menuCss,
|
|
4027
|
+
modeCardCss,
|
|
4028
|
+
onThemeChange,
|
|
4029
|
+
paginationCss,
|
|
4030
|
+
popoverCss,
|
|
4031
|
+
progressBarCss,
|
|
4032
|
+
radioCss,
|
|
4033
|
+
rangeSliderCss,
|
|
4034
|
+
renderMenuEntries,
|
|
4035
|
+
resetTheme,
|
|
4036
|
+
sectionCss,
|
|
4037
|
+
selectCss,
|
|
4038
|
+
separatorCss,
|
|
4039
|
+
setTheme,
|
|
4040
|
+
skeletonCss,
|
|
4041
|
+
sliderCss,
|
|
4042
|
+
spinnerCss,
|
|
4043
|
+
swatchCss,
|
|
4044
|
+
switchCss,
|
|
4045
|
+
tableCss,
|
|
4046
|
+
tabsCss,
|
|
4047
|
+
toast,
|
|
4048
|
+
toastCss,
|
|
4049
|
+
toggleCss,
|
|
4050
|
+
toggleGroupCss,
|
|
4051
|
+
tooltipCss,
|
|
4052
|
+
treeCss
|
|
4053
|
+
});
|
|
4054
|
+
//# sourceMappingURL=index.cjs.map
|