vira 28.1.0 → 28.2.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.
@@ -15,4 +15,5 @@ export * from './vira-icon.element.js';
15
15
  export * from './vira-image.element.js';
16
16
  export * from './vira-input.element.js';
17
17
  export * from './vira-link.element.js';
18
+ export * from './vira-modal.element.js';
18
19
  export * from './vira-progress.element.js';
@@ -15,4 +15,5 @@ export * from './vira-icon.element.js';
15
15
  export * from './vira-image.element.js';
16
16
  export * from './vira-input.element.js';
17
17
  export * from './vira-link.element.js';
18
+ export * from './vira-modal.element.js';
18
19
  export * from './vira-progress.element.js';
@@ -0,0 +1,39 @@
1
+ import { type PartialWithUndefined } from '@augment-vir/common';
2
+ /**
3
+ * A modal element that uses the built-in `dialog` element.
4
+ *
5
+ * @category Elements
6
+ * @see https://electrovir.github.io/vira/book/elements/vira-modal
7
+ */
8
+ export declare const ViraModal: import("element-vir").DeclarativeElementDefinition<"vira-modal", Readonly<{
9
+ open: boolean;
10
+ } & PartialWithUndefined<{
11
+ /** If this isn't set, make sure to use the modal title slot to fill it in. */
12
+ modalTitle: string;
13
+ /**
14
+ * If `true`, the following conditions trigger the modal to close:
15
+ *
16
+ * - The user clicks the "x" close button
17
+ * - The `open` input is set to `false`
18
+ *
19
+ * If set to `false` (the default), the following conditions trigger the modal to close:
20
+ *
21
+ * - The user clicks outside of the modal
22
+ * - The user presses the "Escape" key
23
+ * - The user clicks the "x" close button
24
+ * - The `open` input is set to `false`
25
+ *
26
+ * @default false
27
+ */
28
+ blockLightDismissal: boolean;
29
+ modalSubtitle: string;
30
+ isMobileSize: boolean;
31
+ }>>, {
32
+ dialogElement: HTMLDialogElement | undefined;
33
+ contentElement: HTMLDivElement | undefined;
34
+ previousOpenValue: undefined | boolean;
35
+ /** Remove listeners. */
36
+ cleanup: undefined | (() => void);
37
+ }, {
38
+ modalClose: import("element-vir").DefineEvent<void>;
39
+ }, "vira-modal-phone-size", "vira-modal-backdrop-color" | "vira-modal-backdrop-filter" | "vira-modal-subtitle-color" | "vira-modal-close-button-hover-radius" | "vira-modal-close-button-hover-background-color", readonly ["modalTitle"], readonly []>;
@@ -0,0 +1,223 @@
1
+ import { assertWrap } from '@augment-vir/assert';
2
+ import { css, defineElementEvent, html, listen, nothing, onDomCreated } from 'element-vir';
3
+ import { listenToGlobal } from 'typed-event-target';
4
+ import { X24Icon } from '../icons/icon-svgs/x-24.icon.js';
5
+ import { noNativeFormStyles, noNativeSpacing } from '../styles/native-styles.js';
6
+ import { viraShadows } from '../styles/shadows.js';
7
+ import { defineViraElement } from './define-vira-element.js';
8
+ import { ViraIcon } from './vira-icon.element.js';
9
+ const globalEventsToCloseModalOn = [
10
+ 'pagehide',
11
+ 'pageshow',
12
+ 'popstate',
13
+ ];
14
+ /**
15
+ * A modal element that uses the built-in `dialog` element.
16
+ *
17
+ * @category Elements
18
+ * @see https://electrovir.github.io/vira/book/elements/vira-modal
19
+ */
20
+ export const ViraModal = defineViraElement()({
21
+ tagName: 'vira-modal',
22
+ events: {
23
+ modalClose: defineElementEvent(),
24
+ },
25
+ state() {
26
+ return {
27
+ dialogElement: undefined,
28
+ contentElement: undefined,
29
+ previousOpenValue: undefined,
30
+ /** Remove listeners. */
31
+ cleanup: undefined,
32
+ };
33
+ },
34
+ cleanup({ state }) {
35
+ state.cleanup?.();
36
+ },
37
+ hostClasses: {
38
+ 'vira-modal-phone-size': ({ inputs }) => !!inputs.isMobileSize,
39
+ },
40
+ slotNames: ['modalTitle'],
41
+ cssVars: {
42
+ 'vira-modal-backdrop-color': 'rgba(0, 0, 0, 0.35)',
43
+ 'vira-modal-backdrop-filter': 'blur(3px)',
44
+ 'vira-modal-subtitle-color': '#7E7E7E',
45
+ 'vira-modal-close-button-hover-radius': '8px',
46
+ 'vira-modal-close-button-hover-background-color': '#E4E4E4',
47
+ },
48
+ styles: ({ hostClasses, cssVars }) => css `
49
+ :host {
50
+ display: contents;
51
+ min-width: 280px;
52
+ border-radius: 16px;
53
+ }
54
+
55
+ h1 {
56
+ ${noNativeSpacing};
57
+ }
58
+
59
+ dialog {
60
+ border: none;
61
+ flex-direction: column;
62
+ border-radius: inherit;
63
+ padding: 0;
64
+ overflow: hidden;
65
+ min-width: inherit;
66
+ min-height: inherit;
67
+ max-width: calc(100dvw - 100px);
68
+ max-height: calc(100dvh - 100px);
69
+ ${viraShadows.modal}
70
+
71
+ &[open] {
72
+ display: flex;
73
+ }
74
+ &::backdrop {
75
+ background: ${cssVars['vira-modal-backdrop-color'].value};
76
+ backdrop-filter: ${cssVars['vira-modal-backdrop-filter'].value};
77
+ }
78
+
79
+ & .modal-content-wrapper {
80
+ overflow: hidden;
81
+ display: flex;
82
+ flex-direction: column;
83
+
84
+ & .header {
85
+ padding: 16px 24px;
86
+ display: flex;
87
+ gap: 16px;
88
+ align-items: flex-start;
89
+
90
+ & .header-text-wrapper {
91
+ display: flex;
92
+ flex-direction: column;
93
+ gap: 4px;
94
+ align-self: center;
95
+ margin-right: auto;
96
+
97
+ & h1 {
98
+ font-size: 24px;
99
+ }
100
+
101
+ & sub {
102
+ font-size: 16px;
103
+ color: ${cssVars['vira-modal-subtitle-color'].value};
104
+ }
105
+ }
106
+
107
+ & button.close {
108
+ ${noNativeFormStyles};
109
+ cursor: pointer;
110
+ padding: 4px;
111
+ border-radius: ${cssVars['vira-modal-close-button-hover-radius'].value};
112
+
113
+ &:hover {
114
+ background-color: ${cssVars['vira-modal-close-button-hover-background-color'].value};
115
+ }
116
+
117
+ & ${ViraIcon} {
118
+ display: flex;
119
+ }
120
+ }
121
+ }
122
+ & .body {
123
+ padding: 16px 24px;
124
+ overflow: auto;
125
+ overscroll-behavior: contain;
126
+ }
127
+ }
128
+ }
129
+
130
+ ${hostClasses['vira-modal-phone-size'].selector} {
131
+ & dialog {
132
+ width: 100dvw;
133
+ max-width: 100dvw;
134
+ }
135
+ }
136
+ `,
137
+ render({ inputs, state, updateState, events, dispatch, slotNames }) {
138
+ if (state.dialogElement && inputs.open !== state.dialogElement.open) {
139
+ if (inputs.open) {
140
+ state.dialogElement.showModal();
141
+ }
142
+ else {
143
+ state.dialogElement.close();
144
+ }
145
+ }
146
+ if (state.previousOpenValue !== inputs.open) {
147
+ state.cleanup?.();
148
+ updateState({ previousOpenValue: inputs.open });
149
+ if (inputs.open) {
150
+ const removers = globalEventsToCloseModalOn.map((eventName) => listenToGlobal(eventName, () => {
151
+ dispatch(new events.modalClose());
152
+ }));
153
+ updateState({
154
+ cleanup: () => {
155
+ removers.forEach((remover) => remover());
156
+ },
157
+ });
158
+ }
159
+ }
160
+ function close() {
161
+ if (inputs.open) {
162
+ state.cleanup?.();
163
+ dispatch(new events.modalClose());
164
+ }
165
+ }
166
+ return html `
167
+ <dialog
168
+ ${onDomCreated((element) => {
169
+ updateState({ dialogElement: assertWrap.instanceOf(element, HTMLDialogElement) });
170
+ })}
171
+ ${listen('close', () => {
172
+ close();
173
+ })}
174
+ ${listen('click', (event) => {
175
+ if (state.contentElement &&
176
+ !event.composedPath().includes(state.contentElement) &&
177
+ !inputs.blockLightDismissal) {
178
+ /** Background click. */
179
+ close();
180
+ }
181
+ })}
182
+ >
183
+ <div
184
+ class="modal-content-wrapper"
185
+ ${onDomCreated((element) => {
186
+ updateState({
187
+ contentElement: assertWrap.instanceOf(element, HTMLDivElement),
188
+ });
189
+ })}
190
+ >
191
+ <div class="header">
192
+ <div class="header-text-wrapper">
193
+ <h1><slot name=${slotNames.modalTitle}>${inputs.modalTitle}</slot></h1>
194
+ ${inputs.modalSubtitle
195
+ ? html `
196
+ <sub>${inputs.modalSubtitle}</sub>
197
+ `
198
+ : nothing}
199
+ </div>
200
+ <button
201
+ class="close"
202
+ aria-label="Close"
203
+ ${listen('click', () => {
204
+ state.dialogElement?.close();
205
+ })}
206
+ >
207
+ <${ViraIcon.assign({
208
+ icon: X24Icon,
209
+ })}></${ViraIcon}>
210
+ </button>
211
+ </div>
212
+ ${inputs.open
213
+ ? html `
214
+ <div class="body">
215
+ <slot></slot>
216
+ </div>
217
+ `
218
+ : nothing}
219
+ </div>
220
+ </dialog>
221
+ `;
222
+ },
223
+ });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * A plain X icon.
3
+ *
4
+ * @category Icon
5
+ * @category SVG
6
+ * @see https://electrovir.github.io/vira/book/icons/x24icon
7
+ */
8
+ export declare const X24Icon: import("../icon-svg.js").ViraIconSvg;
@@ -0,0 +1,29 @@
1
+ import { html } from 'element-vir';
2
+ import { viraIconCssVars } from '../icon-css-vars.js';
3
+ import { defineIcon } from '../icon-svg.js';
4
+ /**
5
+ * A plain X icon.
6
+ *
7
+ * @category Icon
8
+ * @category SVG
9
+ * @see https://electrovir.github.io/vira/book/icons/x24icon
10
+ */
11
+ export const X24Icon = defineIcon({
12
+ name: 'X24Icon',
13
+ svgTemplate: html `
14
+ <svg
15
+ xmlns="http://www.w3.org/2000/svg"
16
+ width="24"
17
+ height="24"
18
+ viewBox="0 0 24 24"
19
+ fill=${viraIconCssVars['vira-icon-fill-color'].value}
20
+ stroke=${viraIconCssVars['vira-icon-stroke-color'].value}
21
+ stroke-width=${viraIconCssVars['vira-icon-stroke-width'].value}
22
+ stroke-linecap="round"
23
+ stroke-linejoin="round"
24
+ aria-hidden="true"
25
+ >
26
+ <path d="M18 6L6 18M6 6l12 12" />
27
+ </svg>
28
+ `,
29
+ });
@@ -31,6 +31,7 @@ export * from './icon-svgs/status-success-24.icon.js';
31
31
  export * from './icon-svgs/status-unknown-24.icon.js';
32
32
  export * from './icon-svgs/status-warning-24.icon.js';
33
33
  export * from './icon-svgs/upload-24.icon.js';
34
+ export * from './icon-svgs/x-24.icon.js';
34
35
  /**
35
36
  * All icons within vira by name.
36
37
  *
@@ -67,4 +68,5 @@ export declare const allIconsByName: {
67
68
  readonly StatusUnknown24Icon: import("./icon-svg.js").ViraIconSvg;
68
69
  readonly StatusWarning24Icon: import("./icon-svg.js").ViraIconSvg;
69
70
  readonly Upload24Icon: import("./icon-svg.js").ViraIconSvg;
71
+ readonly X24Icon: import("./icon-svg.js").ViraIconSvg;
70
72
  };
@@ -29,6 +29,7 @@ import { StatusSuccess24Icon } from './icon-svgs/status-success-24.icon.js';
29
29
  import { StatusUnknown24Icon } from './icon-svgs/status-unknown-24.icon.js';
30
30
  import { StatusWarning24Icon } from './icon-svgs/status-warning-24.icon.js';
31
31
  import { Upload24Icon } from './icon-svgs/upload-24.icon.js';
32
+ import { X24Icon } from './icon-svgs/x-24.icon.js';
32
33
  export * from './icon-css-vars.js';
33
34
  export * from './icon-svg.js';
34
35
  export * from './icon-svgs/bell-24.icon.js';
@@ -61,6 +62,7 @@ export * from './icon-svgs/status-success-24.icon.js';
61
62
  export * from './icon-svgs/status-unknown-24.icon.js';
62
63
  export * from './icon-svgs/status-warning-24.icon.js';
63
64
  export * from './icon-svgs/upload-24.icon.js';
65
+ export * from './icon-svgs/x-24.icon.js';
64
66
  /**
65
67
  * All icons within vira by name.
66
68
  *
@@ -97,4 +99,5 @@ export const allIconsByName = {
97
99
  StatusUnknown24Icon,
98
100
  StatusWarning24Icon,
99
101
  Upload24Icon,
102
+ X24Icon,
100
103
  };
@@ -1,4 +1,13 @@
1
1
  import { type CSSResult } from 'element-vir';
2
+ /**
3
+ * CSS Vars for shadows.
4
+ *
5
+ * @category Styles
6
+ */
7
+ export declare const shadowCssVars: import("lit-css-vars").CssVarDefinitions<{
8
+ readonly 'menu-shadow-color': "#e2e2e2";
9
+ readonly 'modal-shadow-color': "#bfbfbf";
10
+ }>;
2
11
  /**
3
12
  * CSS chunks for default Vira shadow styles.
4
13
  *
@@ -7,4 +16,5 @@ import { type CSSResult } from 'element-vir';
7
16
  export declare const viraShadows: {
8
17
  readonly menuShadow: CSSResult;
9
18
  readonly menuShadowReversed: CSSResult;
19
+ readonly modal: CSSResult;
10
20
  };
@@ -1,5 +1,14 @@
1
1
  import { css } from 'element-vir';
2
- const shadowColor = css `#e2e2e2`;
2
+ import { defineCssVars } from 'lit-css-vars';
3
+ /**
4
+ * CSS Vars for shadows.
5
+ *
6
+ * @category Styles
7
+ */
8
+ export const shadowCssVars = defineCssVars({
9
+ 'menu-shadow-color': '#e2e2e2',
10
+ 'modal-shadow-color': '#bfbfbf',
11
+ });
3
12
  /**
4
13
  * CSS chunks for default Vira shadow styles.
5
14
  *
@@ -7,17 +16,20 @@ const shadowColor = css `#e2e2e2`;
7
16
  */
8
17
  export const viraShadows = {
9
18
  menuShadow: css `
10
- filter: drop-shadow(0px 5px 5px ${shadowColor});
19
+ filter: drop-shadow(0px 5px 5px ${shadowCssVars['menu-shadow-color'].value});
11
20
  /*
12
21
  This helps force the drop shadow to re-render when the element moves or the page changes.
13
22
  */
14
23
  will-change: filter;
15
24
  `,
16
25
  menuShadowReversed: css `
17
- filter: drop-shadow(0px -5px 5px ${shadowColor});
26
+ filter: drop-shadow(0px -5px 5px ${shadowCssVars['menu-shadow-color'].value});
18
27
  /*
19
28
  This helps force the drop shadow to re-render when the element moves or the page changes.
20
29
  */
21
30
  will-change: filter;
22
31
  `,
32
+ modal: css `
33
+ box-shadow: 0 20px 60px ${shadowCssVars['modal-shadow-color'].value};
34
+ `,
23
35
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vira",
3
- "version": "28.1.0",
3
+ "version": "28.2.0",
4
4
  "description": "A simple and highly versatile design system using element-vir.",
5
5
  "keywords": [
6
6
  "design",
@@ -38,21 +38,21 @@
38
38
  "test:docs": "virmator docs check"
39
39
  },
40
40
  "dependencies": {
41
- "@augment-vir/assert": "^31.45.0",
42
- "@augment-vir/common": "^31.45.0",
43
- "@augment-vir/web": "^31.45.0",
41
+ "@augment-vir/assert": "^31.47.0",
42
+ "@augment-vir/common": "^31.47.0",
43
+ "@augment-vir/web": "^31.47.0",
44
44
  "colorjs.io": "^0.5.2",
45
45
  "date-vir": "^8.0.0",
46
46
  "device-navigation": "^4.5.5",
47
47
  "lit-css-vars": "^3.0.11",
48
48
  "observavir": "^2.1.2",
49
- "page-active": "^1.0.2",
49
+ "page-active": "^1.0.3",
50
50
  "spa-router-vir": "^6.3.1",
51
51
  "type-fest": "^5.1.0",
52
52
  "typed-event-target": "^4.1.0"
53
53
  },
54
54
  "devDependencies": {
55
- "@augment-vir/test": "^31.45.0",
55
+ "@augment-vir/test": "^31.47.0",
56
56
  "@web/dev-server-esbuild": "^1.0.4",
57
57
  "@web/test-runner": "^0.20.2",
58
58
  "@web/test-runner-commands": "^0.9.0",