vira 31.25.1 → 31.26.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.
@@ -19,6 +19,12 @@ export declare const ViraMenuItem: import("element-vir").DeclarativeElementDefin
19
19
  * _always_ be shown, even if `selected` is set to `false`.
20
20
  */
21
21
  iconOverride: ViraIconSvg;
22
+ /**
23
+ * When `true`, activating this item will _not_ close the containing pop-up.
24
+ *
25
+ * @default false
26
+ */
27
+ keepOpenAfterInteraction: boolean;
22
28
  }>, {
23
29
  /** Removes event listeners registered during init. */
24
30
  cleanupListeners: undefined | (() => void);
@@ -119,6 +119,20 @@ export const ViraMenuItem = defineViraElement()({
119
119
  event.stopPropagation();
120
120
  return;
121
121
  }
122
+ /**
123
+ * Only forward `click`. Slotted content is activated by a single forwarded click, so
124
+ * forwarding `mousedown` as well would double-trigger interactions: a `<select>`, for
125
+ * example, opens its picker via `showPicker()` on both `mousedown` and `click`, and the
126
+ * first call consumes the transient user activation so the second throws
127
+ * `NotAllowedError`. `mousedown` still reaches this handler so disabled items can block
128
+ * it above.
129
+ */
130
+ if (event.type !== 'click') {
131
+ return;
132
+ }
133
+ if (inputs.keepOpenAfterInteraction) {
134
+ event.stopPropagation();
135
+ }
122
136
  const slotElement = assertWrap.instanceOf(host.shadowRoot.querySelector('slot'), HTMLSlotElement);
123
137
  slotElement
124
138
  .assignedElements({
@@ -129,29 +143,23 @@ export const ViraMenuItem = defineViraElement()({
129
143
  event.preventDefault();
130
144
  event.stopPropagation();
131
145
  propagating[event.type] = true;
132
- if (event.type === 'click') {
133
- const hasModifiers = event instanceof MouseEvent &&
134
- (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey);
135
- if (hasModifiers) {
136
- /**
137
- * If modifier keys are present, dispatch a synthetic MouseEvent so
138
- * that the modifier keys are preserved. This is needed so that
139
- * cmd+click opens links in a new tab.
140
- */
141
- element.dispatchEvent(new MouseEvent('click', event));
142
- }
143
- else {
144
- /**
145
- * Use `.click()` instead of dispatching a synthetic MouseEvent so
146
- * that the resulting event is trusted and carries user activation.
147
- * This is required for APIs like `showPicker()` on `<select>`
148
- * elements.
149
- */
150
- element.click();
151
- }
146
+ const hasModifiers = event instanceof MouseEvent &&
147
+ (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey);
148
+ if (hasModifiers) {
149
+ /**
150
+ * If modifier keys are present, dispatch a synthetic MouseEvent so that
151
+ * the modifier keys are preserved. This is needed so that cmd+click
152
+ * opens links in a new tab.
153
+ */
154
+ element.dispatchEvent(new MouseEvent('click', event));
152
155
  }
153
156
  else {
154
- element.dispatchEvent(new MouseEvent(event.type, event));
157
+ /**
158
+ * Use `.click()` instead of dispatching a synthetic MouseEvent so that
159
+ * the resulting event carries user activation. This is required for
160
+ * APIs like `showPicker()` on `<select>` elements.
161
+ */
162
+ element.click();
155
163
  }
156
164
  delete propagating[event.type];
157
165
  }
@@ -160,16 +168,10 @@ export const ViraMenuItem = defineViraElement()({
160
168
  const listenerRemovers = [
161
169
  listenTo(host, 'click', propagateMouseEvent),
162
170
  listenTo(host, 'mousedown', propagateMouseEvent),
163
- /**
164
- * Emit `select` on a non-disabled activation so the containing pop-up can close. This
165
- * uses the _capture_ phase because interactive slotted content can stop the click from
166
- * bubbling back up to this host (which would prevent a bubble-phase listener from ever
167
- * running). The `propagating` guard skips the synthetic click that
168
- * `propagateMouseEvent` re-dispatches onto slotted content, so `select` fires exactly
169
- * once per activation.
170
- */
171
171
  listenTo(host, 'click', (event) => {
172
- if (propagating[event.type] || inputs.disabled) {
172
+ if (propagating[event.type] ||
173
+ inputs.disabled ||
174
+ inputs.keepOpenAfterInteraction) {
173
175
  return;
174
176
  }
175
177
  dispatch(new events.activate(undefined));
@@ -1,4 +1,5 @@
1
1
  import { css, html } from 'element-vir';
2
+ import { viraFontCssVars } from '../styles/font.js';
2
3
  import { defineViraElement } from '../util/define-vira-element.js';
3
4
  /**
4
5
  * Use this element to reserve space for bolded text, even if it isn't bold yet.
@@ -8,7 +9,7 @@ import { defineViraElement } from '../util/define-vira-element.js';
8
9
  export const ViraBoldText = defineViraElement()({
9
10
  tagName: 'vira-bold',
10
11
  cssVars: {
11
- 'vira-bold-bold-weight': 'bold',
12
+ 'vira-bold-bold-weight': viraFontCssVars['vira-font-weight-bold'].value,
12
13
  },
13
14
  hostClasses: {
14
15
  'vira-bold-bold': ({ inputs }) => inputs.bold,
@@ -1,5 +1,6 @@
1
1
  import { css, defineElementEvent, html, listen, nothing, testId } from 'element-vir';
2
2
  import { ChevronUp16Icon } from '../icons/index.js';
3
+ import { viraFontCssVars } from '../styles/font.js';
3
4
  import { viraFormCssVars } from '../styles/form-styles.js';
4
5
  import { defineViraElement } from '../util/define-vira-element.js';
5
6
  import { ViraCollapsibleWrapper } from './vira-collapsible-wrapper.element.js';
@@ -70,7 +71,7 @@ export const ViraCollapsibleCard = defineViraElement()({
70
71
  }
71
72
 
72
73
  .card-header {
73
- font-weight: bold;
74
+ font-weight: ${viraFontCssVars['vira-font-weight-bold'].value};
74
75
  display: flex;
75
76
  align-items: center;
76
77
  justify-content: space-between;
@@ -1,4 +1,5 @@
1
1
  import { css, html } from 'element-vir';
2
+ import { viraFontCssVars } from '../styles/font.js';
2
3
  import { viraFormCssVars } from '../styles/form-styles.js';
3
4
  import { defineViraElement } from '../util/define-vira-element.js';
4
5
  /**
@@ -10,7 +11,7 @@ import { defineViraElement } from '../util/define-vira-element.js';
10
11
  export const ViraError = defineViraElement()({
11
12
  tagName: 'vira-error',
12
13
  cssVars: {
13
- 'vira-error-font-weight': 'bold',
14
+ 'vira-error-font-weight': viraFontCssVars['vira-font-weight-bold'].value,
14
15
  },
15
16
  styles: ({ cssVars }) => css `
16
17
  :host {
@@ -5,6 +5,7 @@ import { attributes, css, defineElementEvent, html, ifDefined, listen, nothing,
5
5
  import { CloseX24Icon } from '../icons/icon-svgs/24/close-x-24.icon.js';
6
6
  import { EyeClosed24Icon, EyeOpen24Icon } from '../icons/index.js';
7
7
  import { createFocusStyles } from '../styles/focus.js';
8
+ import { viraFontCssVars } from '../styles/font.js';
8
9
  import { viraFormCssVars } from '../styles/form-styles.js';
9
10
  import { ViraSize, viraSizeHeights } from '../styles/form-variants.js';
10
11
  import { noUserSelect, viraAnimationDurations, viraDisabledStyles } from '../styles/index.js';
@@ -203,7 +204,7 @@ export const ViraInput = defineViraElement()({
203
204
  }
204
205
 
205
206
  .suffix {
206
- font-weight: bold;
207
+ font-weight: ${viraFontCssVars['vira-font-weight-bold'].value};
207
208
  ${noUserSelect};
208
209
  }
209
210
 
@@ -1,6 +1,17 @@
1
1
  import { type PartialWithUndefined } from '@augment-vir/common';
2
2
  import { type JsonValue } from 'type-fest';
3
3
  import { ViraJsonType, type ViraJsonSchema } from '../util/vira-json-schema.js';
4
+ /**
5
+ * Editing mode for a string field that offers both a fixed set of enum options and free-form text.
6
+ *
7
+ * @category Internal
8
+ */
9
+ export declare enum ViraJsonStringMode {
10
+ /** Pick from the schema's enum options via a dropdown. */
11
+ Options = "options",
12
+ /** Enter an arbitrary string via a text input. */
13
+ Custom = "custom"
14
+ }
4
15
  /**
5
16
  * An editor for arbitrary JSON values, optionally constrained by a standard JSON Schema
6
17
  * ({@link ViraJsonSchema}).
@@ -16,6 +27,7 @@ export declare const ViraJsonForm: import("element-vir").DeclarativeElementDefin
16
27
  pendingKeys: Readonly<Record<string, string>>;
17
28
  pendingTypes: Readonly<Record<string, ViraJsonType>>;
18
29
  pendingArrayValues: Readonly<Record<string, JsonValue>>;
30
+ stringModes: Readonly<Record<string, ViraJsonStringMode>>;
19
31
  showRaw: boolean;
20
32
  rawDraft: string | undefined;
21
33
  rawError: string | undefined;
@@ -6,13 +6,25 @@ import { viraFontCssVars } from '../styles/font.js';
6
6
  import { viraFormCssVars } from '../styles/form-styles.js';
7
7
  import { ViraColorVariant, ViraEmphasis, ViraSize } from '../styles/form-variants.js';
8
8
  import { defineViraElement } from '../util/define-vira-element.js';
9
- import { createDefaultForJsonType, createResolveContext, deleteValueAtPath, getAdditionalPropertiesSchema, getAllowedJsonTypes, getDefinedProperties, getEnumValues, getItemSchema, getJsonType, getNewItemSchema, getPropertySchema, getRequiredProperties, getSchemaTitle, pathToKey, pickBranchForType, setValueAtPath, validateAgainstSchema, ViraJsonType, viraJsonTypeLabels, } from '../util/vira-json-schema.js';
9
+ import { allowsFreeformString, createDefaultForJsonType, createResolveContext, deleteValueAtPath, getAdditionalPropertiesSchema, getAllowedJsonTypes, getDefinedProperties, getItemSchema, getJsonType, getNewItemSchema, getPropertySchema, getRequiredProperties, getSchemaEnumValues, getSchemaTitle, getStringEnumValues, pathToKey, pickBranchForType, setValueAtPath, validateAgainstSchema, ViraJsonType, viraJsonTypeLabels, } from '../util/vira-json-schema.js';
10
10
  import { ViraButton } from './vira-button.element.js';
11
11
  import { ViraCheckbox } from './vira-checkbox.element.js';
12
12
  import { ViraError } from './vira-error.element.js';
13
13
  import { ViraInput, ViraInputType } from './vira-input.element.js';
14
14
  import { ViraSelect } from './vira-select.element.js';
15
15
  import { ViraTextArea } from './vira-text-area.element.js';
16
+ /**
17
+ * Editing mode for a string field that offers both a fixed set of enum options and free-form text.
18
+ *
19
+ * @category Internal
20
+ */
21
+ export var ViraJsonStringMode;
22
+ (function (ViraJsonStringMode) {
23
+ /** Pick from the schema's enum options via a dropdown. */
24
+ ViraJsonStringMode["Options"] = "options";
25
+ /** Enter an arbitrary string via a text input. */
26
+ ViraJsonStringMode["Custom"] = "custom";
27
+ })(ViraJsonStringMode || (ViraJsonStringMode = {}));
16
28
  /**
17
29
  * An editor for arbitrary JSON values, optionally constrained by a standard JSON Schema
18
30
  * ({@link ViraJsonSchema}).
@@ -29,6 +41,7 @@ export const ViraJsonForm = defineViraElement()({
29
41
  pendingKeys: {},
30
42
  pendingTypes: {},
31
43
  pendingArrayValues: {},
44
+ stringModes: {},
32
45
  showRaw: false,
33
46
  rawDraft: undefined,
34
47
  rawError: undefined,
@@ -129,7 +142,7 @@ export const ViraJsonForm = defineViraElement()({
129
142
  }
130
143
 
131
144
  .json-type-tag {
132
- font-weight: normal;
145
+ font-weight: ${viraFontCssVars['vira-font-weight-normal'].value};
133
146
  color: ${viraFormCssVars['vira-form-secondary-body-foreground'].value};
134
147
  }
135
148
 
@@ -238,6 +251,23 @@ export const ViraJsonForm = defineViraElement()({
238
251
  },
239
252
  });
240
253
  }
254
+ function getStringMode(pathKey, value, enumValues) {
255
+ const stored = state.stringModes[pathKey];
256
+ if (stored) {
257
+ return stored;
258
+ }
259
+ return enumValues.includes(value)
260
+ ? ViraJsonStringMode.Options
261
+ : ViraJsonStringMode.Custom;
262
+ }
263
+ function setStringMode(pathKey, mode) {
264
+ updateState({
265
+ stringModes: {
266
+ ...state.stringModes,
267
+ [pathKey]: mode,
268
+ },
269
+ });
270
+ }
241
271
  function clearPending(pathKey) {
242
272
  updateState({
243
273
  pendingKeys: omitObjectKeys(state.pendingKeys, [pathKey]),
@@ -260,7 +290,7 @@ export const ViraJsonForm = defineViraElement()({
260
290
  }
261
291
  function renderPrimitive(path, value, schema) {
262
292
  const type = getJsonType(value);
263
- const enumValues = getEnumValues(schema, resolveContext);
293
+ const enumValues = getSchemaEnumValues(schema, resolveContext);
264
294
  if (enumValues && enumValues.length > 0) {
265
295
  const options = enumValues.map((entry) => {
266
296
  const asString = jsonPrimitiveToString(entry);
@@ -734,6 +764,64 @@ export const ViraJsonForm = defineViraElement()({
734
764
  </table>
735
765
  `;
736
766
  }
767
+ function renderStringEnumOrRaw(path, value, enumValues) {
768
+ const pathKey = pathToKey(path);
769
+ const mode = getStringMode(pathKey, value, enumValues);
770
+ const editor = mode === ViraJsonStringMode.Options
771
+ ? html `
772
+ <${ViraSelect.assign({
773
+ options: enumValues.map((entry) => {
774
+ return {
775
+ value: entry,
776
+ label: entry,
777
+ };
778
+ }),
779
+ value: enumValues.includes(value) ? value : undefined,
780
+ disabled: isDisabled,
781
+ })}
782
+ ${listen(ViraSelect.events.valueChange, (event) => {
783
+ emitReplaceAt(path, event.detail);
784
+ })}
785
+ ></${ViraSelect}>
786
+ `
787
+ : html `
788
+ <${ViraInput.assign({
789
+ value,
790
+ disabled: isDisabled,
791
+ })}
792
+ ${listen(ViraInput.events.valueChange, (event) => {
793
+ emitReplaceAt(path, event.detail);
794
+ })}
795
+ ></${ViraInput}>
796
+ `;
797
+ if (isDisabled) {
798
+ return editor;
799
+ }
800
+ const modeOptions = [
801
+ {
802
+ value: ViraJsonStringMode.Options,
803
+ label: 'Options',
804
+ },
805
+ {
806
+ value: ViraJsonStringMode.Custom,
807
+ label: 'Custom',
808
+ },
809
+ ];
810
+ return html `
811
+ <div class="json-value-with-switcher">
812
+ <span class="json-value-editor-slot">${editor}</span>
813
+ <${ViraSelect.assign({
814
+ options: modeOptions,
815
+ value: mode,
816
+ })}
817
+ title="Choose from options or enter a custom value"
818
+ ${listen(ViraSelect.events.valueChange, (event) => {
819
+ setStringMode(pathKey, event.detail);
820
+ })}
821
+ ></${ViraSelect}>
822
+ </div>
823
+ `;
824
+ }
737
825
  function renderValue(path, value, schema) {
738
826
  const concreteType = getJsonType(value);
739
827
  const allowedTypes = getAllowedJsonTypes(schema, resolveContext);
@@ -744,6 +832,19 @@ export const ViraJsonForm = defineViraElement()({
744
832
  else if (check.isObject(value)) {
745
833
  return renderObjectGroup(path, value, narrowedSchema);
746
834
  }
835
+ /**
836
+ * When a string field constrains values to an enum but also permits free-form strings,
837
+ * offer both: an options dropdown plus a switcher to toggle to raw text entry.
838
+ */
839
+ const isStringOnlyField = allowedTypes.length === 1 && allowedTypes[0] === ViraJsonType.String;
840
+ const stringEnumValues = isStringOnlyField
841
+ ? getStringEnumValues(schema, resolveContext)
842
+ : [];
843
+ if (check.isString(value) &&
844
+ stringEnumValues.length > 0 &&
845
+ allowsFreeformString(schema, resolveContext)) {
846
+ return renderStringEnumOrRaw(path, value, stringEnumValues);
847
+ }
747
848
  const editor = renderPrimitive(path, value, narrowedSchema);
748
849
  const showSwitcher = !isDisabled &&
749
850
  allowedTypes.length > 1 &&
@@ -1,7 +1,7 @@
1
1
  import { assertWrap } from '@augment-vir/assert';
2
2
  import { attributes, css, html, ifDefined, listen, } from 'element-vir';
3
3
  import { listenTo } from 'typed-event-target';
4
- import { viraFormCssVars } from '../styles/form-styles.js';
4
+ import { viraTheme } from '../styles/vira-color-theme.js';
5
5
  import { defineViraElement } from '../util/define-vira-element.js';
6
6
  /**
7
7
  * A hyperlink wrapper element that can be configured to emit route change events rather than just
@@ -41,12 +41,12 @@ export const ViraLink = defineViraElement()({
41
41
  ${hostClasses['vira-link-link-styles'].selector} {
42
42
  &:hover a,
43
43
  & a:hover {
44
- color: ${viraFormCssVars['vira-form-accent-primary-color'].value};
44
+ color: ${viraTheme.colors['vira-blue-foreground-non-body'].foreground.value};
45
45
  }
46
46
 
47
47
  &:active a,
48
48
  & a:active {
49
- color: ${viraFormCssVars['vira-form-accent-primary-active-color'].value};
49
+ color: ${viraTheme.colors['vira-blue-foreground-body'].foreground.value};
50
50
  }
51
51
  }
52
52
  `,
@@ -20,6 +20,13 @@ export declare const ViraRelativeTime: import("element-vir").DeclarativeElementD
20
20
  * @default {seconds: 5}
21
21
  */
22
22
  updateInterval: Readonly<AtLeastOneDuration>;
23
+ /**
24
+ * Timezone the absolute time is displayed in. Defaults to the user's timezone. Set this for
25
+ * values that are conceptually anchored to a specific timezone (e.g. a date-only value
26
+ * stored at midnight UTC), where converting to the user's timezone would shift the
27
+ * displayed date/time.
28
+ */
29
+ timezone: string;
23
30
  }>, {
24
31
  now: {
25
32
  hour: import("date-vir").Hour;
@@ -76,11 +76,18 @@ export const ViraRelativeTime = defineViraElement()({
76
76
  decimalCount: 0,
77
77
  });
78
78
  return html `
79
- <span title=${formatAbsoluteTime(inputs.time)}>${relativeTime}</span>
79
+ <span
80
+ title=${formatAbsoluteTime(inputs.time, {
81
+ timezone: inputs.timezone,
82
+ })}
83
+ >
84
+ ${relativeTime}
85
+ </span>
80
86
  ${inputs.showAbsoluteTime
81
87
  ? html `
82
88
  <${ViraAbsoluteTime.assign({
83
89
  time: inputs.time,
90
+ timezone: inputs.timezone,
84
91
  })}></${ViraAbsoluteTime}>
85
92
  `
86
93
  : nothing}
@@ -44,7 +44,25 @@ export type ViraTab = {
44
44
  * current route.
45
45
  */
46
46
  exactMatch: boolean;
47
+ /**
48
+ * Optional cluster label. Consecutive tabs sharing the same `group` string render together
49
+ * under that label, separated from other clusters by an inset vertical divider. Tabs with no
50
+ * `group` render standalone (no label, no divider). Grouping only affects rendering when at
51
+ * least one tab has a `group`.
52
+ */
53
+ group: string;
47
54
  }>;
55
+ /**
56
+ * Groups a flat list of tabs into clusters. Consecutive tabs sharing the same `group` string are
57
+ * merged into a single cluster; ungrouped tabs (and tabs whose `group` differs from their
58
+ * predecessor) each start a new cluster.
59
+ *
60
+ * @category Internal
61
+ */
62
+ export declare function buildClusters(tabList: ReadonlyArray<Readonly<ViraTab>>): {
63
+ group: string | undefined;
64
+ tabs: ReadonlyArray<Readonly<ViraTab>>;
65
+ }[];
48
66
  /**
49
67
  * A tab bar element that renders an array of tabs with an animated selection indicator.
50
68
  *
@@ -85,13 +103,22 @@ export declare const ViraTabs: import("element-vir").DeclarativeElementDefinitio
85
103
  menuIsDisabled: boolean;
86
104
  /** Offset for the dropdown pop-up. Only used when tabs overflow into a dropdown. */
87
105
  menuPopUpOffset: Readonly<PopUpOffset>;
106
+ /**
107
+ * Text shown on the overflow "more" trigger when the selected tab is _not_ collapsed into
108
+ * the menu. When the selected tab _is_ collapsed, the trigger shows that tab's label (with
109
+ * a checkmark) instead. Set this to a localized string; it defaults to `'More'`.
110
+ *
111
+ * @default 'More'
112
+ */
113
+ overflowLabel: string;
88
114
  /** When true, tabs and their container expand to fill all available horizontal space. */
89
115
  shouldFillWidth: boolean;
90
116
  }>, {
91
- isOverflowing: boolean;
117
+ /** How many of the visible tabs are collapsed into the overflow "more" menu. */
118
+ overflowCount: number;
92
119
  /** A callback to remove all internal observers. */
93
120
  cleanupObserver: undefined | (() => void);
94
121
  }, {
95
122
  /** Fires when a tab is clicked with the corresponding tab entry. */
96
123
  tabSelect: import("element-vir").DefineEvent<Readonly<ViraTab>>;
97
- }, "vira-tabs-bar-top" | "vira-tabs-bar-bottom" | "vira-tabs-bar-left" | "vira-tabs-bar-right" | "vira-tabs-color-red" | "vira-tabs-color-yellow" | "vira-tabs-color-green" | "vira-tabs-color-blue" | "vira-tabs-color-brand" | "vira-tabs-color-purple" | "vira-tabs-color-plain" | "vira-tabs-color-neutral" | "vira-tabs-color-teal" | "vira-tabs-color-pink" | "vira-tabs-color-grey" | "vira-tabs-icon-layout-vertical" | "vira-tabs-icon-layout-horizontal" | "vira-tabs-overflowing" | "vira-tabs-fill-width", "vira-tabs-active-color" | "vira-tabs-active-hover-color" | "vira-tabs-inactive-color" | "vira-tabs-inactive-hover-color" | "vira-tabs-bar-thickness", readonly [], readonly []>;
124
+ }, "vira-tabs-bar-top" | "vira-tabs-bar-bottom" | "vira-tabs-bar-left" | "vira-tabs-bar-right" | "vira-tabs-color-red" | "vira-tabs-color-yellow" | "vira-tabs-color-green" | "vira-tabs-color-blue" | "vira-tabs-color-brand" | "vira-tabs-color-purple" | "vira-tabs-color-plain" | "vira-tabs-color-neutral" | "vira-tabs-color-teal" | "vira-tabs-color-pink" | "vira-tabs-color-grey" | "vira-tabs-icon-layout-vertical" | "vira-tabs-icon-layout-horizontal" | "vira-tabs-fill-width", "vira-tabs-active-color" | "vira-tabs-active-hover-color" | "vira-tabs-inactive-color" | "vira-tabs-inactive-hover-color" | "vira-tabs-bar-thickness", readonly [], readonly []>;