@inglorious/ui 9.9.0 → 9.10.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/package.json +29 -3
- package/src/controls/checkbox/template.js +3 -1
- package/src/controls/chip-picker/chip-picker.stories.js +22 -0
- package/src/controls/chip-picker/index.js +3 -0
- package/src/controls/chip-picker/style.css +26 -0
- package/src/controls/chip-picker/template.js +53 -0
- package/src/controls/combobox/handlers.js +83 -20
- package/src/controls/combobox/template.js +3 -1
- package/src/controls/controls.css +3 -2
- package/src/controls/radio-group/template.js +3 -1
- package/src/controls/rating/template.js +4 -1
- package/src/controls/select/template.js +6 -12
- package/src/controls/switch/template.js +3 -1
- package/src/data-display/badge/style.css +1 -0
- package/src/data-display/data-display.css +1 -0
- package/src/data-display/lightbox/index.js +3 -0
- package/src/data-display/lightbox/lightbox.stories.js +25 -0
- package/src/data-display/lightbox/style.css +97 -0
- package/src/data-display/lightbox/template.js +153 -0
- package/src/feedback/confirm-dialog/confirm-dialog.stories.js +33 -0
- package/src/feedback/confirm-dialog/index.js +3 -0
- package/src/feedback/confirm-dialog/style.css +16 -0
- package/src/feedback/confirm-dialog/template.js +70 -0
- package/src/feedback/crop-dialog/index.js +3 -0
- package/src/feedback/crop-dialog/style.css +43 -0
- package/src/feedback/crop-dialog/template.js +82 -0
- package/src/feedback/dialog/style.css +2 -1
- package/src/feedback/dialog/template.js +5 -2
- package/src/feedback/feedback.css +3 -0
- package/src/feedback/load-status/index.js +3 -0
- package/src/feedback/load-status/load-status.stories.js +16 -0
- package/src/feedback/load-status/style.css +10 -0
- package/src/feedback/load-status/template.js +43 -0
- package/src/index.js +5 -0
- package/src/shared/icons.js +12 -0
- package/src/shared/live-boolean.js +17 -0
- package/types/controls/chip-picker.d.ts +23 -0
- package/types/controls/combobox.d.ts +20 -2
- package/types/data-display/lightbox.d.ts +35 -0
- package/types/feedback/confirm-dialog.d.ts +34 -0
- package/types/feedback/crop-dialog.d.ts +35 -0
- package/types/feedback/load-status.d.ts +22 -0
- package/types/index.d.ts +17 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/data-display/lightbox').LightboxProps} LightboxProps
|
|
3
|
+
* @typedef {import('../../../types/data-display/lightbox').LightboxImage} LightboxImage
|
|
4
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html, nothing } from "@inglorious/web"
|
|
8
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
9
|
+
import { when } from "@inglorious/web/directives/when"
|
|
10
|
+
|
|
11
|
+
import { IconButton } from "../../controls/icon-button/index.js"
|
|
12
|
+
import { chevronLeft, chevronRight, close } from "../../shared/icons.js"
|
|
13
|
+
import { syncDialog } from "../../shared/top-layer.js"
|
|
14
|
+
import { Icon } from "../icon/index.js"
|
|
15
|
+
|
|
16
|
+
const SWIPE_PX = 40
|
|
17
|
+
const NEXT = 1
|
|
18
|
+
const PREVIOUS = -1
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where each lightbox's current swipe started.
|
|
22
|
+
* @type {WeakMap<HTMLDialogElement, number>}
|
|
23
|
+
*/
|
|
24
|
+
const swipeStarts = new WeakMap()
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders images full screen in a modal `<dialog>`, one at a time. With more
|
|
28
|
+
* than one, the arrows, the arrow keys and a horizontal swipe step through
|
|
29
|
+
* them, following the writing direction. Esc, the close button and a press
|
|
30
|
+
* outside the image close it.
|
|
31
|
+
* @param {LightboxProps} props
|
|
32
|
+
* @returns {TemplateResult | typeof nothing}
|
|
33
|
+
*/
|
|
34
|
+
export function render(props) {
|
|
35
|
+
const {
|
|
36
|
+
id,
|
|
37
|
+
images = [],
|
|
38
|
+
index = 0,
|
|
39
|
+
isOpen = false,
|
|
40
|
+
label = "Photos",
|
|
41
|
+
closeLabel = "Close",
|
|
42
|
+
previousLabel = "Previous photo",
|
|
43
|
+
nextLabel = "Next photo",
|
|
44
|
+
className = "",
|
|
45
|
+
onStep,
|
|
46
|
+
onClose,
|
|
47
|
+
} = props
|
|
48
|
+
|
|
49
|
+
const total = images.length
|
|
50
|
+
if (!total) return nothing
|
|
51
|
+
|
|
52
|
+
const current = ((index % total) + total) % total
|
|
53
|
+
const image = toImage(images[current])
|
|
54
|
+
const isNavigable = total > 1 && Boolean(onStep)
|
|
55
|
+
|
|
56
|
+
return html`<dialog
|
|
57
|
+
id=${id || nothing}
|
|
58
|
+
class=${["iw-lightbox", className].filter(Boolean).join(" ")}
|
|
59
|
+
aria-label=${label}
|
|
60
|
+
@click=${(event) => {
|
|
61
|
+
if (event.target === event.currentTarget) onClose?.()
|
|
62
|
+
}}
|
|
63
|
+
@cancel=${(event) => {
|
|
64
|
+
if (!onClose) event.preventDefault()
|
|
65
|
+
}}
|
|
66
|
+
@keydown=${(event) => {
|
|
67
|
+
if (!isNavigable) return
|
|
68
|
+
const step = arrowStep(event)
|
|
69
|
+
if (!step) return
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
onStep(step)
|
|
72
|
+
}}
|
|
73
|
+
@pointerdown=${(event) => {
|
|
74
|
+
swipeStarts.set(event.currentTarget, event.clientX)
|
|
75
|
+
}}
|
|
76
|
+
@pointerup=${(event) => {
|
|
77
|
+
const startX = swipeStarts.get(event.currentTarget)
|
|
78
|
+
swipeStarts.delete(event.currentTarget)
|
|
79
|
+
if (!isNavigable || startX == null) return
|
|
80
|
+
const distance = event.clientX - startX
|
|
81
|
+
if (Math.abs(distance) < SWIPE_PX) return
|
|
82
|
+
const towardsEnd = isRtl(event.currentTarget)
|
|
83
|
+
? distance > 0
|
|
84
|
+
: distance < 0
|
|
85
|
+
onStep(towardsEnd ? NEXT : PREVIOUS)
|
|
86
|
+
}}
|
|
87
|
+
@pointercancel=${(event) => swipeStarts.delete(event.currentTarget)}
|
|
88
|
+
${ref((element) => syncDialog(element, { isOpen, onClose }))}
|
|
89
|
+
>
|
|
90
|
+
${when(onClose, () =>
|
|
91
|
+
IconButton.render({
|
|
92
|
+
icon: Icon.render({ icon: close, size: "lg" }),
|
|
93
|
+
variant: "ghost",
|
|
94
|
+
shape: "round",
|
|
95
|
+
ariaLabel: closeLabel,
|
|
96
|
+
className: "iw-lightbox-close",
|
|
97
|
+
onClick: onClose,
|
|
98
|
+
}),
|
|
99
|
+
)}
|
|
100
|
+
<div class="iw-lightbox-stage">
|
|
101
|
+
<img
|
|
102
|
+
class="iw-lightbox-image"
|
|
103
|
+
src=${image.src}
|
|
104
|
+
alt=${image.alt ?? ""}
|
|
105
|
+
draggable="false"
|
|
106
|
+
/>
|
|
107
|
+
${when(
|
|
108
|
+
isNavigable,
|
|
109
|
+
() => html`
|
|
110
|
+
${IconButton.render({
|
|
111
|
+
icon: Icon.render({ icon: chevronLeft, size: "lg" }),
|
|
112
|
+
variant: "ghost",
|
|
113
|
+
shape: "round",
|
|
114
|
+
ariaLabel: previousLabel,
|
|
115
|
+
className: "iw-lightbox-nav iw-lightbox-previous",
|
|
116
|
+
onClick: () => onStep(PREVIOUS),
|
|
117
|
+
})}
|
|
118
|
+
${IconButton.render({
|
|
119
|
+
icon: Icon.render({ icon: chevronRight, size: "lg" }),
|
|
120
|
+
variant: "ghost",
|
|
121
|
+
shape: "round",
|
|
122
|
+
ariaLabel: nextLabel,
|
|
123
|
+
className: "iw-lightbox-nav iw-lightbox-next",
|
|
124
|
+
onClick: () => onStep(NEXT),
|
|
125
|
+
})}
|
|
126
|
+
<div class="iw-lightbox-counter" aria-hidden="true">
|
|
127
|
+
${current + 1}/${total}
|
|
128
|
+
</div>
|
|
129
|
+
`,
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</dialog>`
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @param {string | LightboxImage} image
|
|
137
|
+
* @returns {LightboxImage}
|
|
138
|
+
*/
|
|
139
|
+
function toImage(image) {
|
|
140
|
+
return typeof image === "string" ? { src: image } : image
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function arrowStep(event) {
|
|
144
|
+
const towardsEnd = isRtl(event.currentTarget) ? "ArrowLeft" : "ArrowRight"
|
|
145
|
+
const towardsStart = isRtl(event.currentTarget) ? "ArrowRight" : "ArrowLeft"
|
|
146
|
+
if (event.key === towardsEnd) return NEXT
|
|
147
|
+
if (event.key === towardsStart) return PREVIOUS
|
|
148
|
+
return 0
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function isRtl(element) {
|
|
152
|
+
return getComputedStyle(element).direction === "rtl"
|
|
153
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRender } from "../../stories/notify-story.js"
|
|
2
|
+
import { ConfirmDialog } from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Feedback/ConfirmDialog",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
render: createRender(ConfirmDialog),
|
|
8
|
+
argTypes: {
|
|
9
|
+
tone: { control: "select", options: ["danger", "primary"] },
|
|
10
|
+
isBusy: { control: "boolean" },
|
|
11
|
+
onConfirm: { action: "onConfirm" },
|
|
12
|
+
onCancel: { action: "onCancel" },
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Danger = {
|
|
17
|
+
args: {
|
|
18
|
+
isOpen: true,
|
|
19
|
+
message: "Delete this event? This cannot be undone.",
|
|
20
|
+
confirmLabel: "Delete",
|
|
21
|
+
busyLabel: "Deleting…",
|
|
22
|
+
cancelLabel: "Keep it",
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const Primary = {
|
|
27
|
+
args: {
|
|
28
|
+
isOpen: true,
|
|
29
|
+
tone: "primary",
|
|
30
|
+
message: "Publish this event now?",
|
|
31
|
+
confirmLabel: "Publish",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/feedback/confirm-dialog').ConfirmDialogProps} ConfirmDialogProps
|
|
3
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html } from "@inglorious/web"
|
|
7
|
+
|
|
8
|
+
import { Button } from "../../controls/button/index.js"
|
|
9
|
+
import { Dialog } from "../dialog/index.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Renders a sheet asking to confirm an action, with the confirm button first.
|
|
13
|
+
* While `isBusy` it cannot be dismissed, and the confirm button shows
|
|
14
|
+
* `busyLabel`.
|
|
15
|
+
* @param {ConfirmDialogProps} props
|
|
16
|
+
* @returns {TemplateResult}
|
|
17
|
+
*/
|
|
18
|
+
export function render(props) {
|
|
19
|
+
const {
|
|
20
|
+
id,
|
|
21
|
+
isOpen = false,
|
|
22
|
+
message,
|
|
23
|
+
children,
|
|
24
|
+
confirmLabel,
|
|
25
|
+
busyLabel = confirmLabel,
|
|
26
|
+
cancelLabel = "Cancel",
|
|
27
|
+
closeLabel,
|
|
28
|
+
tone = "danger",
|
|
29
|
+
isBusy = false,
|
|
30
|
+
isDisabled = false,
|
|
31
|
+
className = "",
|
|
32
|
+
onConfirm,
|
|
33
|
+
onCancel,
|
|
34
|
+
} = props
|
|
35
|
+
|
|
36
|
+
const isDanger = tone === "danger"
|
|
37
|
+
|
|
38
|
+
return Dialog.render({
|
|
39
|
+
id,
|
|
40
|
+
isOpen,
|
|
41
|
+
variant: "sheet",
|
|
42
|
+
size: "sm",
|
|
43
|
+
description: message,
|
|
44
|
+
children,
|
|
45
|
+
className: ["iw-confirm-dialog", className].filter(Boolean).join(" "),
|
|
46
|
+
closeLabel,
|
|
47
|
+
actions: html`<div class="iw-confirm-dialog-actions">
|
|
48
|
+
${Button.render({
|
|
49
|
+
children: isBusy ? busyLabel : confirmLabel,
|
|
50
|
+
color: isDanger ? "error" : "primary",
|
|
51
|
+
variant: isDanger ? "outline" : "default",
|
|
52
|
+
shape: "pill",
|
|
53
|
+
size: "md",
|
|
54
|
+
isDisabled: isBusy || isDisabled,
|
|
55
|
+
onClick: () => {
|
|
56
|
+
if (!isBusy && !isDisabled) onConfirm?.()
|
|
57
|
+
},
|
|
58
|
+
})}
|
|
59
|
+
${Button.render({
|
|
60
|
+
children: cancelLabel,
|
|
61
|
+
variant: "outline",
|
|
62
|
+
shape: "pill",
|
|
63
|
+
size: "md",
|
|
64
|
+
isDisabled: isBusy,
|
|
65
|
+
onClick: onCancel,
|
|
66
|
+
})}
|
|
67
|
+
</div>`,
|
|
68
|
+
onClose: isBusy ? undefined : onCancel,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
.iw-dialog.iw-crop-dialog {
|
|
2
|
+
width: min(48rem, calc(100vw - 2 * var(--iw-space-4)));
|
|
3
|
+
min-width: 0;
|
|
4
|
+
max-width: none;
|
|
5
|
+
max-height: calc(
|
|
6
|
+
100dvh - 2 * var(--iw-space-4) - var(--safe-area-top, 0px) -
|
|
7
|
+
var(--safe-area-bottom, 0px)
|
|
8
|
+
);
|
|
9
|
+
overflow-x: hidden;
|
|
10
|
+
overflow-y: auto;
|
|
11
|
+
overscroll-behavior: contain;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.iw-crop-dialog-viewport {
|
|
15
|
+
width: 100%;
|
|
16
|
+
--iw-image-cropper-max-height: 55dvh;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (max-height: 32rem) {
|
|
20
|
+
.iw-crop-dialog-viewport {
|
|
21
|
+
--iw-image-cropper-max-height: 35dvh;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.iw-crop-dialog-error {
|
|
26
|
+
margin: var(--iw-space-3) 0 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.iw-crop-dialog-actions {
|
|
30
|
+
display: flex;
|
|
31
|
+
justify-content: flex-end;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: var(--iw-space-2);
|
|
34
|
+
width: 100%;
|
|
35
|
+
|
|
36
|
+
.iw-button {
|
|
37
|
+
flex: 0 1 auto;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
padding-inline: var(--iw-space-3);
|
|
40
|
+
font-size: var(--iw-font-size-sm);
|
|
41
|
+
white-space: nowrap;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/feedback/crop-dialog').CropDialogProps} CropDialogProps
|
|
3
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
|
+
* @typedef {import('@inglorious/web').Api} Api
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { html } from "@inglorious/web"
|
|
8
|
+
import { when } from "@inglorious/web/directives/when"
|
|
9
|
+
|
|
10
|
+
import { Button } from "../../controls/button/index.js"
|
|
11
|
+
import { Alert } from "../alert/index.js"
|
|
12
|
+
import { Dialog } from "../dialog/index.js"
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Renders an ImageCropper entity in a dialog, with cancel and apply buttons.
|
|
16
|
+
* Apply stays disabled until the image has loaded, and passes the cropper's
|
|
17
|
+
* `rect`.
|
|
18
|
+
* @param {CropDialogProps} props
|
|
19
|
+
* @param {Api} api
|
|
20
|
+
* @returns {TemplateResult}
|
|
21
|
+
*/
|
|
22
|
+
export function render(props, api) {
|
|
23
|
+
const {
|
|
24
|
+
id,
|
|
25
|
+
isOpen = false,
|
|
26
|
+
cropperId,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
applyLabel = "Apply",
|
|
30
|
+
savingLabel = "Saving…",
|
|
31
|
+
cancelLabel = "Cancel",
|
|
32
|
+
closeLabel,
|
|
33
|
+
loadErrorText = "The image could not be loaded.",
|
|
34
|
+
error,
|
|
35
|
+
isSaving = false,
|
|
36
|
+
className = "",
|
|
37
|
+
onCancel,
|
|
38
|
+
onApply,
|
|
39
|
+
} = props
|
|
40
|
+
|
|
41
|
+
const cropper = api.getEntity(cropperId)
|
|
42
|
+
const isLoaded = Boolean(cropper?.naturalWidth)
|
|
43
|
+
const shownError = error || (cropper?.hasError ? loadErrorText : null)
|
|
44
|
+
|
|
45
|
+
return Dialog.render({
|
|
46
|
+
id,
|
|
47
|
+
isOpen,
|
|
48
|
+
title,
|
|
49
|
+
description,
|
|
50
|
+
closeLabel,
|
|
51
|
+
className: ["iw-crop-dialog", className].filter(Boolean).join(" "),
|
|
52
|
+
onClose: onCancel,
|
|
53
|
+
children: html`
|
|
54
|
+
<div class="iw-crop-dialog-viewport">${api.render(cropperId)}</div>
|
|
55
|
+
${when(shownError, () =>
|
|
56
|
+
Alert.render({
|
|
57
|
+
severity: "error",
|
|
58
|
+
className: "iw-crop-dialog-error",
|
|
59
|
+
children: shownError,
|
|
60
|
+
}),
|
|
61
|
+
)}
|
|
62
|
+
`,
|
|
63
|
+
actions: html`<div class="iw-crop-dialog-actions">
|
|
64
|
+
${Button.render({
|
|
65
|
+
children: cancelLabel,
|
|
66
|
+
variant: "ghost",
|
|
67
|
+
shape: "pill",
|
|
68
|
+
size: "sm",
|
|
69
|
+
isDisabled: isSaving,
|
|
70
|
+
onClick: onCancel,
|
|
71
|
+
})}
|
|
72
|
+
${Button.render({
|
|
73
|
+
children: isSaving ? savingLabel : applyLabel,
|
|
74
|
+
color: "primary",
|
|
75
|
+
shape: "pill",
|
|
76
|
+
size: "sm",
|
|
77
|
+
isDisabled: isSaving || !isLoaded,
|
|
78
|
+
onClick: () => onApply?.(cropper.rect),
|
|
79
|
+
})}
|
|
80
|
+
</div>`,
|
|
81
|
+
})
|
|
82
|
+
}
|
|
@@ -53,6 +53,9 @@ export function render(props) {
|
|
|
53
53
|
|
|
54
54
|
const titleId = id && title ? `${id}-title` : ""
|
|
55
55
|
const descriptionId = id && description ? `${id}-description` : ""
|
|
56
|
+
// Without a title, the description is what the dialog asks: its name.
|
|
57
|
+
const labelId = titleId || descriptionId
|
|
58
|
+
const describedById = titleId ? descriptionId : ""
|
|
56
59
|
const onBackdrop = onBackdropClick ?? onClose
|
|
57
60
|
|
|
58
61
|
return html`<dialog
|
|
@@ -63,8 +66,8 @@ export function render(props) {
|
|
|
63
66
|
"iw-dialog-sheet": variant === "sheet",
|
|
64
67
|
...extraClasses,
|
|
65
68
|
})}
|
|
66
|
-
aria-labelledby=${
|
|
67
|
-
aria-describedby=${
|
|
69
|
+
aria-labelledby=${labelId || nothing}
|
|
70
|
+
aria-describedby=${describedById || nothing}
|
|
68
71
|
@pointerdown=${(event) => {
|
|
69
72
|
backdropPresses.set(
|
|
70
73
|
event.currentTarget,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
@import "./alert/style.css";
|
|
2
2
|
@import "./backdrop/style.css";
|
|
3
|
+
@import "./confirm-dialog/style.css";
|
|
4
|
+
@import "./crop-dialog/style.css";
|
|
3
5
|
@import "./dialog/style.css";
|
|
6
|
+
@import "./load-status/style.css";
|
|
4
7
|
@import "./progress/style.css";
|
|
5
8
|
@import "./skeleton/style.css";
|
|
6
9
|
@import "./snackbar/style.css";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRender } from "../../stories/notify-story.js"
|
|
2
|
+
import { LoadStatus } from "."
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
title: "Feedback/LoadStatus",
|
|
6
|
+
tags: ["autodocs"],
|
|
7
|
+
render: createRender(LoadStatus),
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Loading = { args: { isLoading: true } }
|
|
11
|
+
|
|
12
|
+
export const Empty = { args: { isEmpty: true, emptyText: "No events yet." } }
|
|
13
|
+
|
|
14
|
+
export const Error = {
|
|
15
|
+
args: { isError: true, error: "The events could not be loaded." },
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
.iw-load-status {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: center;
|
|
5
|
+
grid-column: 1 / -1;
|
|
6
|
+
padding: var(--iw-space-10) 0;
|
|
7
|
+
text-align: center;
|
|
8
|
+
font-size: var(--iw-font-size-sm);
|
|
9
|
+
color: var(--iw-color-text-muted, var(--iw-color-text));
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('../../../types/feedback/load-status').LoadStatusProps} LoadStatusProps
|
|
3
|
+
* @typedef {import('@inglorious/web').TemplateResult} TemplateResult
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { html, nothing } from "@inglorious/web"
|
|
7
|
+
|
|
8
|
+
import { Progress } from "../progress/index.js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders where a load stands instead of what it loads: an error, a loader or
|
|
12
|
+
* an empty message, in that order of precedence. Otherwise it renders
|
|
13
|
+
* `content`, a function so that it runs only once there is something to show.
|
|
14
|
+
* @param {LoadStatusProps} props
|
|
15
|
+
* @returns {import('../../../types/shared/renderable').Renderable}
|
|
16
|
+
*/
|
|
17
|
+
export function render(props) {
|
|
18
|
+
const {
|
|
19
|
+
isError = false,
|
|
20
|
+
error,
|
|
21
|
+
isLoading = false,
|
|
22
|
+
loader,
|
|
23
|
+
loadingLabel = "Loading…",
|
|
24
|
+
isEmpty = false,
|
|
25
|
+
emptyText,
|
|
26
|
+
content,
|
|
27
|
+
className = "",
|
|
28
|
+
} = props
|
|
29
|
+
|
|
30
|
+
const classes = ["iw-load-status", className].filter(Boolean).join(" ")
|
|
31
|
+
|
|
32
|
+
if (isError) return html`<div class=${classes} role="alert">${error}</div>`
|
|
33
|
+
if (isLoading) {
|
|
34
|
+
return html`<div class=${classes} role="status">
|
|
35
|
+
${
|
|
36
|
+
loader ??
|
|
37
|
+
Progress.render({ variant: "circular", ariaLabel: loadingLabel })
|
|
38
|
+
}
|
|
39
|
+
</div>`
|
|
40
|
+
}
|
|
41
|
+
if (isEmpty) return html`<div class=${classes}>${emptyText}</div>`
|
|
42
|
+
return content ? content() : nothing
|
|
43
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { Button } from "./controls/button/index.js"
|
|
2
2
|
export { ButtonGroup } from "./controls/button-group/index.js"
|
|
3
3
|
export { Checkbox } from "./controls/checkbox/index.js"
|
|
4
|
+
export { ChipPicker } from "./controls/chip-picker/index.js"
|
|
4
5
|
export { Fab } from "./controls/fab/index.js"
|
|
5
6
|
export { FileUpload } from "./controls/file-upload/index.js"
|
|
6
7
|
export { IconButton } from "./controls/icon-button/index.js"
|
|
@@ -25,6 +26,7 @@ export { Chip } from "./data-display/chip/index.js"
|
|
|
25
26
|
export { DataGrid } from "./data-display/data-grid/index.js"
|
|
26
27
|
export { Divider } from "./data-display/divider/index.js"
|
|
27
28
|
export { Icon } from "./data-display/icon/index.js"
|
|
29
|
+
export { Lightbox } from "./data-display/lightbox/index.js"
|
|
28
30
|
export { List } from "./data-display/list/index.js"
|
|
29
31
|
export { Table } from "./data-display/table/index.js"
|
|
30
32
|
export { Tooltip } from "./data-display/tooltip/index.js"
|
|
@@ -32,7 +34,10 @@ export { Typography } from "./data-display/typography/index.js"
|
|
|
32
34
|
export { VirtualList } from "./data-display/virtual-list/index.js"
|
|
33
35
|
export { Alert } from "./feedback/alert/index.js"
|
|
34
36
|
export { Backdrop } from "./feedback/backdrop/index.js"
|
|
37
|
+
export { ConfirmDialog } from "./feedback/confirm-dialog/index.js"
|
|
38
|
+
export { CropDialog } from "./feedback/crop-dialog/index.js"
|
|
35
39
|
export { Dialog } from "./feedback/dialog/index.js"
|
|
40
|
+
export { LoadStatus } from "./feedback/load-status/index.js"
|
|
36
41
|
export { Progress } from "./feedback/progress/index.js"
|
|
37
42
|
export { Skeleton } from "./feedback/skeleton/index.js"
|
|
38
43
|
export { Snackbar } from "./feedback/snackbar/index.js"
|
package/src/shared/icons.js
CHANGED
|
@@ -9,3 +9,15 @@ export const leftPanelClose = {
|
|
|
9
9
|
height: 24,
|
|
10
10
|
body: '<path fill="currentColor" d="M16.5 16V8l-4 4zM5 21q-.825 0-1.412-.587T3 19V5q0-.825.588-1.412T5 3h14q.825 0 1.413.588T21 5v14q0 .825-.587 1.413T19 21zm3-2V5H5v14zm2 0h9V5h-9zm-2 0H5z"/>',
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
export const chevronLeft = {
|
|
14
|
+
width: 24,
|
|
15
|
+
height: 24,
|
|
16
|
+
body: '<path fill="currentColor" d="m14 18l-6-6l6-6l1.4 1.4l-4.6 4.6l4.6 4.6z"/>',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const chevronRight = {
|
|
20
|
+
width: 24,
|
|
21
|
+
height: 24,
|
|
22
|
+
body: '<path fill="currentColor" d="M12.6 12L8 7.4L9.4 6l6 6l-6 6L8 16.6z"/>',
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ref } from "@inglorious/web/directives/ref"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Keeps an element's `checked` or `selected` property in step with the state.
|
|
5
|
+
*
|
|
6
|
+
* A `.checked=${false}` binding can't be used instead: server rendering
|
|
7
|
+
* reflects it as `checked="false"`, which HTML reads as checked. Templates pair
|
|
8
|
+
* this with the boolean attribute (`?checked=${value}`), which renders right on
|
|
9
|
+
* the server but stops counting once the user has touched the control.
|
|
10
|
+
* @param {"checked" | "selected"} name
|
|
11
|
+
* @param {boolean} value
|
|
12
|
+
*/
|
|
13
|
+
export function liveBoolean(name, value) {
|
|
14
|
+
return ref((element) => {
|
|
15
|
+
if (element) element[name] = value
|
|
16
|
+
})
|
|
17
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TemplateResult } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
import type { ComboboxOption } from "./combobox"
|
|
4
|
+
|
|
5
|
+
export interface ChipPickerProps {
|
|
6
|
+
/** The fieldset's legend. */
|
|
7
|
+
label?: TemplateResult | string
|
|
8
|
+
hint?: TemplateResult | string
|
|
9
|
+
/** Strings, numbers or `{ value, label, isDisabled }`, as in Combobox. */
|
|
10
|
+
options?: ComboboxOption[]
|
|
11
|
+
/** The values picked. */
|
|
12
|
+
value?: (string | number)[]
|
|
13
|
+
isDisabled?: boolean
|
|
14
|
+
className?: string
|
|
15
|
+
/** Called with the value of the chip pressed, picked or not. */
|
|
16
|
+
onToggle?: (value: string | number) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ChipPickerType = {
|
|
20
|
+
render(props: ChipPickerProps): TemplateResult
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export declare const ChipPicker: ChipPickerType
|
|
@@ -45,6 +45,16 @@ export interface ComboboxProps {
|
|
|
45
45
|
noOptionsMessage?: string
|
|
46
46
|
loadingMessage?: string
|
|
47
47
|
|
|
48
|
+
/** A validation message, shown under the control. */
|
|
49
|
+
error?: string | null
|
|
50
|
+
/**
|
|
51
|
+
* An event to notify when the user picks or clears a value, such as
|
|
52
|
+
* `"#form:fieldChange"`. The payload is `changePayload` with the new `value`.
|
|
53
|
+
*/
|
|
54
|
+
changeEvent?: string
|
|
55
|
+
/** Merged into `changeEvent`'s payload, such as `{ path: "country" }`. */
|
|
56
|
+
changePayload?: Record<string, unknown>
|
|
57
|
+
|
|
48
58
|
isFullWidth?: boolean
|
|
49
59
|
size?: "sm" | "md" | "lg"
|
|
50
60
|
onToggle?: () => void
|
|
@@ -68,13 +78,21 @@ export type ComboboxType = {
|
|
|
68
78
|
open(entity: ComboboxProps): void
|
|
69
79
|
close(entity: ComboboxProps): void
|
|
70
80
|
toggle(entity: ComboboxProps): void
|
|
71
|
-
optionSelect(entity: ComboboxProps, option: ComboboxOption): void
|
|
72
|
-
clear(entity: ComboboxProps): void
|
|
81
|
+
optionSelect(entity: ComboboxProps, option: ComboboxOption, api?: Api): void
|
|
82
|
+
clear(entity: ComboboxProps, payload?: unknown, api?: Api): void
|
|
73
83
|
searchChange(entity: ComboboxProps, searchTerm: string): void
|
|
74
84
|
focusNext(entity: ComboboxProps): void
|
|
75
85
|
focusPrev(entity: ComboboxProps): void
|
|
76
86
|
focusFirst(entity: ComboboxProps): void
|
|
77
87
|
focusLast(entity: ComboboxProps): void
|
|
88
|
+
focusSet(entity: ComboboxProps, index: number): void
|
|
89
|
+
/** Sets the value from outside, without notifying `changeEvent`. An empty string is no value. */
|
|
90
|
+
valueSet(
|
|
91
|
+
entity: ComboboxProps,
|
|
92
|
+
value: string | number | (string | number)[] | null,
|
|
93
|
+
): void
|
|
94
|
+
optionsSet(entity: ComboboxProps, options: ComboboxOption[]): void
|
|
95
|
+
errorSet(entity: ComboboxProps, error: string | null): void
|
|
78
96
|
}
|
|
79
97
|
|
|
80
98
|
export declare const Combobox: ComboboxType
|