@inglorious/ui 8.4.2 → 8.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/ui",
3
- "version": "8.4.2",
3
+ "version": "8.6.0",
4
4
  "description": "A design system for Inglorious Web with render-function components and customizable themes",
5
5
  "author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
6
6
  "license": "MIT",
@@ -308,19 +308,19 @@
308
308
  "peerDependencies": {
309
309
  "@inglorious/store": "12.0.0",
310
310
  "@inglorious/utils": "5.1.0",
311
- "@inglorious/web": "6.8.1"
311
+ "@inglorious/web": "6.9.0"
312
312
  },
313
313
  "devDependencies": {
314
314
  "@iconify-icons/fa7-brands": "^2.0.0",
315
315
  "@iconify-icons/fa7-regular": "^2.0.0",
316
316
  "@iconify-icons/fa7-solid": "^2.0.0",
317
317
  "@iconify-icons/material-symbols": "^2.0.6",
318
- "@inglorious/charts": "11.1.6",
318
+ "@inglorious/charts": "11.1.7",
319
319
  "@inglorious/eslint-config": "1.2.0",
320
320
  "@inglorious/store": "12.0.0",
321
321
  "@inglorious/vite-plugin-jsx": "2.0.5",
322
322
  "@inglorious/vite-plugin-vue": "1.0.6",
323
- "@inglorious/web": "6.8.1",
323
+ "@inglorious/web": "6.9.0",
324
324
  "@storybook/addon-docs": "^10.6.0",
325
325
  "@storybook/web-components-vite": "^10.6.0",
326
326
  "happy-dom": "^20.14.0",
@@ -57,7 +57,19 @@ export default {
57
57
  options: ["button", "submit", "reset", "menu"],
58
58
  description: "Native HTML button type attribute.",
59
59
  },
60
+ accept: {
61
+ control: "text",
62
+ description: "File types the picker offers (with `onFileSelect`).",
63
+ },
64
+ isMultiple: {
65
+ control: "boolean",
66
+ description: "Lets the picker select more than one file.",
67
+ },
60
68
  onClick: { action: "onClick" },
69
+ onFileSelect: {
70
+ action: "onFileSelect",
71
+ description: "Turns the button into a file picker.",
72
+ },
61
73
  },
62
74
  parameters: {
63
75
  docs: {
@@ -150,6 +162,16 @@ Round.args = {
150
162
  children: "+",
151
163
  }
152
164
 
165
+ export const FilePicker = {}
166
+ FilePicker.args = {
167
+ ...Default.args,
168
+ children: "Change photo",
169
+ variant: "outline",
170
+ shape: "pill",
171
+ accept: "image/jpeg,image/png,image/webp",
172
+ onFileSelect: () => {},
173
+ }
174
+
153
175
  export const CustomContent = {}
154
176
  CustomContent.args = {
155
177
  ...Default.args,
@@ -3,7 +3,7 @@
3
3
  * @typedef {import('@inglorious/web').TemplateResult} TemplateResult
4
4
  */
5
5
 
6
- import { html } from "@inglorious/web"
6
+ import { html, nothing } from "@inglorious/web"
7
7
  import { classMap } from "@inglorious/web/directives/class-map"
8
8
  import { ref } from "@inglorious/web/directives/ref"
9
9
 
@@ -12,10 +12,14 @@ import { applyElementProps } from "../../shared/apply-element-props.js"
12
12
  /**
13
13
  * Renders a themed button for actions and form submission.
14
14
  * Supports variants, sizes, shapes, pressed state, and full-width layout.
15
+ * With `onFileSelect`, the button also opens a file picker on click.
15
16
  * @example
16
17
  * // Stateless usage
17
18
  * button.render({ children: 'Click me', variant: 'primary' })
18
19
  *
20
+ * // File picker
21
+ * button.render({ children: 'Upload', accept: 'image/*', onFileSelect })
22
+ *
19
23
  * @param {ButtonProps} props
20
24
  * @returns {TemplateResult}
21
25
  */
@@ -34,7 +38,10 @@ export function render(props) {
34
38
  ariaLabel = "",
35
39
  isAriaPressed = false,
36
40
  className = "",
41
+ accept,
42
+ isMultiple = false,
37
43
  onClick,
44
+ onFileSelect,
38
45
  ...rest
39
46
  } = props
40
47
 
@@ -57,6 +64,13 @@ export function render(props) {
57
64
  ...extraClasses,
58
65
  }
59
66
 
67
+ const handleClick = onFileSelect
68
+ ? (event) => {
69
+ onClick?.(event)
70
+ event.currentTarget.nextElementSibling.click()
71
+ }
72
+ : onClick
73
+
60
74
  return html`
61
75
  <button
62
76
  type=${buttonType}
@@ -64,10 +78,29 @@ export function render(props) {
64
78
  aria-pressed=${isAriaPressed}
65
79
  class=${classMap(classes)}
66
80
  ?disabled=${isDisabled}
67
- @click=${onClick}
81
+ @click=${handleClick}
68
82
  ${ref((element) => applyElementProps(element, rest))}
69
83
  >
70
84
  ${children}
71
85
  </button>
86
+ ${
87
+ onFileSelect
88
+ ? html`<input
89
+ type="file"
90
+ accept=${accept ?? ""}
91
+ ?multiple=${isMultiple}
92
+ ?disabled=${isDisabled}
93
+ hidden
94
+ @change=${(event) => handleFileChange(event, onFileSelect)}
95
+ />`
96
+ : nothing
97
+ }
72
98
  `
73
99
  }
100
+
101
+ function handleFileChange(event, onFileSelect) {
102
+ const { files } = event.target
103
+ if (files?.length) onFileSelect(files)
104
+ // Clear the input so picking the same file again still fires `change`
105
+ event.target.value = ""
106
+ }
@@ -58,6 +58,10 @@
58
58
  inset: 0;
59
59
  opacity: 0;
60
60
  cursor: pointer;
61
+
62
+ &::file-selector-button {
63
+ cursor: inherit;
64
+ }
61
65
  }
62
66
 
63
67
  .iw-upload-content {
@@ -10,7 +10,14 @@ export default {
10
10
  message: { control: "text" },
11
11
  position: {
12
12
  control: "select",
13
- options: ["bottom-left", "bottom-right", "top-left", "top-right"],
13
+ options: [
14
+ "bottom-left",
15
+ "bottom-center",
16
+ "bottom-right",
17
+ "top-left",
18
+ "top-center",
19
+ "top-right",
20
+ ],
14
21
  },
15
22
  onClose: { action: "onClose" },
16
23
  },
@@ -23,3 +30,20 @@ export const Default = {
23
30
  position: "bottom-left",
24
31
  },
25
32
  }
33
+
34
+ // A phone app keeps a bar at the bottom: the snackbar rises above it.
35
+ export const AboveBottomNavigation = {
36
+ args: {
37
+ isOpen: true,
38
+ message: "New version available",
39
+ position: "bottom-center",
40
+ },
41
+ decorators: [
42
+ (story) => {
43
+ const container = document.createElement("div")
44
+ container.style.setProperty("--iw-snackbar-offset-bottom", "4.875rem")
45
+ container.append(story())
46
+ return container
47
+ },
48
+ ],
49
+ }
@@ -1,26 +1,47 @@
1
+ /* Fixed to the viewport, clear of the system bars. A bottom snackbar also
2
+ clears whatever bar the app keeps at the bottom: the app sets
3
+ --iw-snackbar-offset-bottom to that bar's height while it shows, and the
4
+ offset then stands in for the safe area, which the bar already covers.
5
+ Only the app knows when its bar is on screen, so nothing is guessed here. */
1
6
  .iw-snackbar {
7
+ --iw-snackbar-gap: var(--iw-space-4);
2
8
  position: fixed;
3
9
  z-index: var(--iw-z-popover, 1060);
10
+ max-width: calc(100vw - 2 * var(--iw-snackbar-gap));
4
11
  }
5
12
 
6
- .iw-snackbar-bottom-left {
7
- left: var(--iw-space-4);
8
- bottom: var(--iw-space-4);
13
+ .iw-snackbar-bottom-left,
14
+ .iw-snackbar-bottom-center,
15
+ .iw-snackbar-bottom-right {
16
+ bottom: calc(
17
+ var(--iw-snackbar-gap) +
18
+ var(--iw-snackbar-offset-bottom, var(--safe-area-bottom, 0px))
19
+ );
9
20
  }
10
21
 
11
- .iw-snackbar-bottom-right {
12
- right: var(--iw-space-4);
13
- bottom: var(--iw-space-4);
22
+ .iw-snackbar-top-left,
23
+ .iw-snackbar-top-center,
24
+ .iw-snackbar-top-right {
25
+ top: calc(var(--iw-snackbar-gap) + var(--safe-area-top, 0px));
14
26
  }
15
27
 
28
+ .iw-snackbar-bottom-left,
16
29
  .iw-snackbar-top-left {
17
- left: var(--iw-space-4);
18
- top: var(--iw-space-4);
30
+ left: var(--iw-snackbar-gap);
19
31
  }
20
32
 
33
+ .iw-snackbar-bottom-right,
21
34
  .iw-snackbar-top-right {
22
- right: var(--iw-space-4);
23
- top: var(--iw-space-4);
35
+ right: var(--iw-snackbar-gap);
36
+ }
37
+
38
+ /* Both insets and auto margins: the snackbar is as wide as its content, up
39
+ to the gutters, and centered within them. */
40
+ .iw-snackbar-bottom-center,
41
+ .iw-snackbar-top-center {
42
+ inset-inline: var(--iw-snackbar-gap);
43
+ width: fit-content;
44
+ margin-inline: auto;
24
45
  }
25
46
 
26
47
  .iw-snackbar-content {
@@ -22,7 +22,13 @@ export interface ButtonProps {
22
22
  ariaLabel?: string
23
23
  isAriaPressed?: boolean
24
24
  className?: string
25
- onClick?: () => void
25
+ /** File types the picker offers, as in the native `accept` attribute */
26
+ accept?: string
27
+ /** Lets the picker select more than one file */
28
+ isMultiple?: boolean
29
+ onClick?: (event: MouseEvent) => void
30
+ /** Turns the button into a file picker; called with the chosen files */
31
+ onFileSelect?: (files: FileList) => void
26
32
  [key: string]: unknown
27
33
  }
28
34
 
@@ -6,7 +6,13 @@ export interface SnackbarProps {
6
6
  isOpen?: boolean
7
7
  message?: TemplateResult | string | number
8
8
  action?: TemplateResult | string | number
9
- position?: "bottom-left" | "bottom-right" | "top-left" | "top-right"
9
+ position?:
10
+ | "bottom-left"
11
+ | "bottom-center"
12
+ | "bottom-right"
13
+ | "top-left"
14
+ | "top-center"
15
+ | "top-right"
10
16
  className?: string
11
17
  onClose?: () => void
12
18
  [key: string]: unknown