@swiftwc/ui 0.0.0-dev.66 → 0.0.0-dev.68

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.
Files changed (40) hide show
  1. package/generated/client/index.d.ts +4 -1
  2. package/generated/client/index.js +50 -35
  3. package/generated/components/h-stack.d.ts +4 -0
  4. package/generated/components/h-stack.js +4 -0
  5. package/generated/components/index.d.ts +1 -0
  6. package/generated/components/index.js +1 -0
  7. package/generated/components/label-view.d.ts +5 -0
  8. package/generated/components/label-view.js +5 -0
  9. package/generated/components/toggle-view.d.ts +28 -0
  10. package/generated/components/toggle-view.js +222 -0
  11. package/generated/components/v-stack.d.ts +4 -0
  12. package/generated/components/v-stack.js +4 -0
  13. package/generated/css/index.css +110 -31
  14. package/generated/events/index.d.ts +1 -0
  15. package/generated/events/index.js +1 -0
  16. package/generated/events/toggle-view.d.ts +9 -0
  17. package/generated/events/toggle-view.js +1 -0
  18. package/generated/tpl/html.d.ts +5 -0
  19. package/generated/tpl/html.js +3 -0
  20. package/generated/tpl/index.d.ts +4 -0
  21. package/generated/tpl/index.js +4 -0
  22. package/generated/tpl/render.d.ts +2 -0
  23. package/generated/tpl/render.js +135 -0
  24. package/package.json +3 -2
  25. package/scss/_components.scss +2 -0
  26. package/scss/components/_date-picker.scss +15 -15
  27. package/scss/components/_h-stack.scss +17 -1
  28. package/scss/components/_index.scss +4 -0
  29. package/scss/components/_menu-view.scss +53 -49
  30. package/scss/components/_picker-view.scss +38 -39
  31. package/scss/components/_scroll-view-proxy.scss +4 -2
  32. package/scss/components/_section-view.scss +23 -22
  33. package/scss/components/_table-view.scss +6 -6
  34. package/scss/components/_text-field.scss +26 -26
  35. package/scss/components/_toggle-view.scss +79 -0
  36. package/scss/components/_tool-bar.scss +52 -52
  37. package/scss/mixins/_index.scss +9 -2
  38. package/scss/placeholders/_lists.scss +14 -12
  39. package/scss/transitions/_menu-view.scss +17 -15
  40. package/web-components.html-data/en.json +175 -0
@@ -11,7 +11,10 @@ export declare const alert: (title?: string, message?: string, actions?: {
11
11
  role?: string;
12
12
  }[], options?: {
13
13
  titleVisibility?: boolean;
14
- }) => Promise<string>;
14
+ signal?: AbortSignal;
15
+ }) => Promise<string | undefined> & {
16
+ dismiss: (reason?: unknown) => void;
17
+ };
15
18
  export declare const confirmationDialog: (trigger: HTMLElement, title: string, message?: string, actions?: {
16
19
  label?: string;
17
20
  image?: string;
@@ -5,6 +5,7 @@ import { NavigationPath } from '../internal/class/navigation-path';
5
5
  import { queryInsertPosition, startViewTransition } from '../internal/privateNamespace';
6
6
  import { $, devFlags, kebabCase, onoff } from '../internal/utils';
7
7
  import { Snapshot } from '../snapshot';
8
+ import { html, render } from '../tpl';
8
9
  //#region polyfills
9
10
  export const polyfills = new Map();
10
11
  for (const [k, Ctor] of Object.entries(Components)) {
@@ -174,47 +175,61 @@ if (mediaQueryList.matches)
174
175
  ])
175
176
  document.addEventListener(k, v, { passive: true });
176
177
  //#region fns
177
- export const alert = async (title, message, actions, options) => {
178
- return await navigator.locks.request('alert:', async () => {
179
- const dialog = $(`<dialog is="alert-dialog"></dialog>`, '>1'), vStack = dialog.querySelector(':scope>v-stack') ?? dialog.appendChild($(`<v-stack spacing="1" alignment="fill"></v-stack>`, '>1'));
180
- if (title) {
181
- const label = $(`<label-view font="headline"></label-view>`, '>1');
182
- label.setAttribute('title', title);
183
- vStack.insertAdjacentElement('beforeend', label);
184
- }
185
- if (message) {
186
- const label = $(`<label-view foreground="secondary" font="callout"></label-view>`, '>1');
187
- label.setAttribute('title', message);
188
- vStack.insertAdjacentElement('beforeend', label);
189
- }
190
- for (const [index, action] of (actions ?? [
191
- {
192
- role: 'close',
193
- },
194
- ]).entries()) {
195
- const btn = $(`<button type="button" tabindex="0" is="bordered-button"></button>`, '>1');
196
- btn.setAttribute('value', `${index}`);
197
- if (action?.role)
198
- btn.setAttribute('role', action.role);
199
- if (action.label || action.image) {
200
- const label = $(`<label-view></label-view>`, '>1');
201
- if (action.label)
202
- label.setAttribute('title', action.label);
203
- if (action.image)
204
- label.setAttribute('system-image', action.image);
205
- btn.appendChild(label);
206
- }
207
- dialog.insertAdjacentElement('beforeend', btn);
208
- }
178
+ // const a = alert('Saving…', 'Please wait')
179
+ // // later, e.g. save completed early
180
+ // a.dismiss() // silently resolves undefined, closes+removes dialog
181
+ // // or
182
+ // a.dismiss(new Error('cancelled')) // rejects instead
183
+ // const result = await a // still works normally if not dismissed
184
+ // const ac = new AbortController()
185
+ // const a = alert('Uploading…', 'Please wait', undefined, { signal: ac.signal })
186
+ // // elsewhere, e.g. upload finished before the user dismissed
187
+ // ac.abort() // dialog closes silently, a rejects with AbortError
188
+ // // or still works standalone
189
+ // a.dismiss() // same effect, no signal needed
190
+ export const alert = (title, message, actions, options) => {
191
+ const { promise, resolve, reject } = Promise.withResolvers();
192
+ let dialog;
193
+ let off;
194
+ const dismiss = (reason) => {
195
+ off?.();
196
+ dialog?.close();
197
+ dialog?.remove();
198
+ reason === undefined ? resolve(undefined) : reject(reason instanceof Error ? reason : new DOMException('Alert dismissed', 'AbortError'));
199
+ };
200
+ if (options?.signal)
201
+ if (options.signal.aborted)
202
+ dismiss(options.signal.reason); // already aborted before we even started — settle immediately, skip showing anything
203
+ else
204
+ options.signal.addEventListener('abort', () => dismiss(options.signal.reason), { once: true });
205
+ navigator.locks
206
+ .request('alert:', async () => {
207
+ if (options?.signal?.aborted)
208
+ return; // in case it was aborted while queued behind another lock holder
209
+ const mount = document.createElement('div');
210
+ render(html `<dialog is="alert-dialog">
211
+ <v-stack spacing="1" alignment="fill">
212
+ ${title ? html `<label-view font="headline" title="${title}"></label-view>` : null}
213
+ ${message ? html `<label-view foreground="secondary" font="callout" title="${message}"></label-view>` : null}
214
+ </v-stack>
215
+ ${(actions ?? [{ role: 'close' }]).map((action, index) => html `
216
+ <button type="button" tabindex="0" is="bordered-button" value="${index}" role="${action.role ?? null}">
217
+ ${action.label || action.image ? html `<label-view title="${action.label ?? null}" system-image="${action.image ?? null}"></label-view>` : null}
218
+ </button>
219
+ `)}
220
+ </dialog>`, mount);
221
+ dialog = mount.firstElementChild;
209
222
  document.body.insertAdjacentElement('beforeend', dialog);
210
223
  dialog.showModal();
211
- const { promise, resolve } = Promise.withResolvers(), off = onoff('alert:return', (evt) => {
224
+ off = onoff('alert:return', (evt) => {
212
225
  const { detail } = evt;
213
- off();
226
+ off?.();
214
227
  resolve(detail.returnValue);
215
228
  }, alertDialog, { once: true }).on();
216
229
  return promise;
217
- });
230
+ })
231
+ .catch(() => { });
232
+ return Object.assign(promise, { dismiss });
218
233
  };
219
234
  export const confirmationDialog = async (trigger, title, message, actions, options) => {
220
235
  const newAnchorName = `--confirmation-dialog-${self.crypto.randomUUID()}`;
@@ -2,6 +2,10 @@
2
2
  *
3
3
  * @attr {@spacingSet} spacing - The gap between the primary axis
4
4
  *
5
+ * @attr {@alignmentSet} alignment - The cross-axis alignment
6
+ * @attr {@distributionSet} distribution - The main-axis alignment
7
+ * @attr {@placementSet} placement - The main-axis alignment
8
+ *
5
9
  */
6
10
  export declare class HStack extends HTMLElement {
7
11
  constructor();
@@ -3,6 +3,10 @@ import { devFlags } from '../internal/utils';
3
3
  *
4
4
  * @attr {@spacingSet} spacing - The gap between the primary axis
5
5
  *
6
+ * @attr {@alignmentSet} alignment - The cross-axis alignment
7
+ * @attr {@distributionSet} distribution - The main-axis alignment
8
+ * @attr {@placementSet} placement - The main-axis alignment
9
+ *
6
10
  */
7
11
  export class HStack extends HTMLElement {
8
12
  constructor() {
@@ -39,6 +39,7 @@ export * from './tab-item';
39
39
  export * from './tab-view';
40
40
  export * from './table-view';
41
41
  export * from './text-field';
42
+ export * from './toggle-view';
42
43
  export * from './tool-bar-item-group';
43
44
  export * from './tool-bar-item';
44
45
  export * from './tool-bar';
@@ -39,6 +39,7 @@ export * from './tab-item';
39
39
  export * from './tab-view';
40
40
  export * from './table-view';
41
41
  export * from './text-field';
42
+ export * from './toggle-view';
42
43
  export * from './tool-bar-item-group';
43
44
  export * from './tool-bar-item';
44
45
  export * from './tool-bar';
@@ -1,3 +1,8 @@
1
+ /**
2
+ *
3
+ * @attr {@fontSet} font - Sets the default font for text in this view.
4
+ *
5
+ */
1
6
  export declare class LabelView extends HTMLElement {
2
7
  #private;
3
8
  static get observedAttributes(): string[];
@@ -1,4 +1,9 @@
1
1
  import { $, devFlags, renderLabelIcon, renderLabelTitle } from '../internal/utils';
2
+ /**
3
+ *
4
+ * @attr {@fontSet} font - Sets the default font for text in this view.
5
+ *
6
+ */
2
7
  export class LabelView extends HTMLElement {
3
8
  static get observedAttributes() {
4
9
  return ['system-image', 'title', 'line-limit', 'truncation-mode'];
@@ -0,0 +1,28 @@
1
+ import { FormAssociatedBase } from '../internal/class/form-associated-base';
2
+ declare const toggleStyles: readonly ["switch", "button"];
3
+ export type ToggleStyle = (typeof toggleStyles)[number];
4
+ /**
5
+ * @slot label
6
+ * @slot validity-options
7
+ */
8
+ export declare class ToggleView extends FormAssociatedBase {
9
+ #private;
10
+ static get observedAttributes(): string[];
11
+ static get template(): DocumentFragment;
12
+ constructor();
13
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
14
+ disconnectedCallback(): void;
15
+ connectedCallback(): void;
16
+ get toggleStyle(): ToggleStyle;
17
+ get isOn(): boolean;
18
+ set isOn(v: boolean);
19
+ get name(): string;
20
+ get value(): string | null;
21
+ set value(v: string | null);
22
+ setValidity: (flags?: ValidityStateFlags, message?: string, anchor?: HTMLElement) => void;
23
+ setCustomValidity: (message: string) => void;
24
+ formAssociatedCallback: (form: HTMLFormElement) => void;
25
+ formDisabledCallback: (disabled: boolean) => void;
26
+ formResetCallback: () => void;
27
+ }
28
+ export {};
@@ -0,0 +1,222 @@
1
+ var _a;
2
+ import { CleanupRegistry } from '../internal/class/cleanup-registry';
3
+ import { FormAssociatedBase, getInternals } from '../internal/class/form-associated-base';
4
+ import { MutationObserverSet } from '../internal/class/mutation-observer-set';
5
+ import { $, devFlags, kebabCase, onoff, renderLabel } from '../internal/utils';
6
+ const toggleStyles = ['switch', 'button'];
7
+ /**
8
+ * @slot label
9
+ * @slot validity-options
10
+ */
11
+ export class ToggleView extends FormAssociatedBase {
12
+ static get observedAttributes() {
13
+ return ['label', 'name', 'value', 'is-on', 'keyboard-type', 'required', 'disabled'];
14
+ }
15
+ static #template;
16
+ static get template() {
17
+ return (this.#template ??= $(String.raw `
18
+ <label part="root toggle-stack">
19
+ <div part="root toggle-label-stack">
20
+ <slot name="label"></slot>
21
+ </div>
22
+ <div part="root toggle-input-stack">
23
+ <!--<input type="checkbox" part="root input toggle-form-input">-->
24
+ <div part="root toggle-form-input" tabindex="0"></div>
25
+ </div>
26
+ <slot name="validity-options" hidden></slot>
27
+ </label>`));
28
+ }
29
+ #renderValidityMsgs = (entries) => {
30
+ if (devFlags.debug)
31
+ console.debug(`${_a.name} ⚡️ mutation`);
32
+ this.setValidity(this.validity, this.validationMessage);
33
+ };
34
+ #shadowRoot;
35
+ #customValidity = '';
36
+ #slots = new Map();
37
+ #validityObservers = new MutationObserverSet(this.#renderValidityMsgs);
38
+ #input;
39
+ #value = 'on';
40
+ #isOn = false;
41
+ #connected = false;
42
+ get #internals() {
43
+ return getInternals(this);
44
+ }
45
+ constructor() {
46
+ super();
47
+ this.#shadowRoot = this.attachShadow({ mode: 'closed' });
48
+ this.#shadowRoot.appendChild(document.importNode(this.constructor.template, true));
49
+ for (const slot of this.#shadowRoot.querySelectorAll('slot'))
50
+ this.#slots?.set(slot.name, slot);
51
+ CleanupRegistry.register(this, () => {
52
+ this.#slots = new Map();
53
+ });
54
+ CleanupRegistry.unregister(this, 'validities');
55
+ CleanupRegistry.register(this, onoff('slotchange', this.#handleValiditiesSlotchange, this.#slots?.get('validity-options')).on(), 'validities');
56
+ this.#input = this.#shadowRoot.querySelector('[part*=toggle-form-input]') ?? undefined;
57
+ this.#input?.addEventListener('click', () => {
58
+ this.isOn = !this.isOn;
59
+ this.#sendValueToForm(this.#connected);
60
+ });
61
+ CleanupRegistry.register(this, onoff([{ types: 'focusin', listener: this.#handleFocusin }], this).on());
62
+ CleanupRegistry.register(this, onoff([{ types: 'keydown', listener: this.#handleInputKeydown }], this.#input).on());
63
+ // CleanupRegistry.register(this, onoff([{ types: 'input', listener: this.#handleInputInput }], this.#input).on())
64
+ }
65
+ attributeChangedCallback(name, oldValue, newValue) {
66
+ if (devFlags.debug)
67
+ console.debug(`${_a.name} ⚡️ attr-change [${name}] ("${oldValue}" → "${newValue}")`);
68
+ switch (name) {
69
+ case 'value':
70
+ this.value = String(newValue); // this.#input?.setAttribute('checked', newValue ?? '')
71
+ this.#sendValueToForm(this.#connected);
72
+ break;
73
+ case 'is-on':
74
+ this.isOn = newValue !== null; // this.#input?.setAttribute('checked', newValue ?? '')
75
+ this.#sendValueToForm(this.#connected);
76
+ break;
77
+ case 'required':
78
+ // this.#input?.setAttribute(name, newValue ?? '')
79
+ this.#sendValueToForm(this.#connected);
80
+ break;
81
+ case 'name':
82
+ this.#sendValueToForm(this.#connected);
83
+ break;
84
+ case 'label':
85
+ renderLabel(':scope>label-view[slot=label]', `<label-view slot="label" foreground="secondary"><span></span></label-view>`, this, newValue);
86
+ break;
87
+ case 'disabled':
88
+ for (const el of this.#shadowRoot.querySelectorAll('input'))
89
+ el.toggleAttribute('disabled', !newValue);
90
+ break;
91
+ }
92
+ }
93
+ disconnectedCallback() {
94
+ super.disconnectedCallback();
95
+ this.#validityObservers.unobserveAll();
96
+ }
97
+ connectedCallback() {
98
+ super.connectedCallback();
99
+ // finally
100
+ // if (this.hasAttribute('value')) this.value = this.getAttribute('value') ?? ''
101
+ if (this.hasAttribute('is-on'))
102
+ this.isOn = this.hasAttribute('is-on');
103
+ this.#sendValueToForm(false);
104
+ this.#connected = true;
105
+ }
106
+ get toggleStyle() {
107
+ return toggleStyles.includes(this.getAttribute('toggle-style') ?? '') ? this.getAttribute('toggle-style') : 'switch';
108
+ }
109
+ get isOn() {
110
+ return this.#isOn;
111
+ }
112
+ set isOn(v) {
113
+ this.#isOn = Boolean(v);
114
+ this.#value = this.#isOn ? (this.getAttribute('value') ?? 'on') : null;
115
+ this.ariaChecked = `${this.#isOn}`;
116
+ }
117
+ #handleValiditiesSlotchange = ({ type, target: slot }) => {
118
+ if (devFlags.debug)
119
+ console.debug(`${_a.name} ⚡️ ${type}`);
120
+ if (!(slot instanceof HTMLSlotElement))
121
+ return;
122
+ const assigned = slot.assignedElements();
123
+ this.#validityObservers.syncObservations(assigned, ['value', 'label']);
124
+ this.#renderValidityMsgs([]);
125
+ };
126
+ #handleFocusin = ({ type, target }) => {
127
+ if (devFlags.debug)
128
+ console.debug(`${_a.name} ⚡️ ${type}`);
129
+ if (target === this)
130
+ this.#input?.focus();
131
+ };
132
+ #sendValueToForm = (dispatchEvent = true) => {
133
+ // input.value has already been updated/synced !!
134
+ if (this.matches(':disabled'))
135
+ return this.setValidity({});
136
+ if (this.hasAttribute('required') && !this.isOn) {
137
+ this.setValidity({ valueMissing: true });
138
+ }
139
+ else {
140
+ this.setValidity({});
141
+ }
142
+ const entries = new FormData();
143
+ if (this.value)
144
+ entries.append(this.name, this.value);
145
+ this.#internals.setFormValue(entries);
146
+ if (dispatchEvent)
147
+ this.dispatchEvent(new CustomEvent('commit', { detail: { value: this.value, isOn: this.isOn }, bubbles: true, composed: true }));
148
+ };
149
+ #handleInputKeydown = (evt) => {
150
+ if (devFlags.debug)
151
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
152
+ this.isOn = !this.isOn;
153
+ this.#sendValueToForm();
154
+ };
155
+ #handleInputInput = (evt) => {
156
+ if (devFlags.debug)
157
+ console.debug(`${_a.name} ⚡️ ${evt?.type}`);
158
+ this.#sendValueToForm();
159
+ };
160
+ // Optional: form participation properties
161
+ get name() {
162
+ return this.getAttribute('name') ?? this.getAttribute('label') ?? this.querySelector(':scope>[slot=label]')?.textContent ?? '';
163
+ }
164
+ get value() {
165
+ return this.#value;
166
+ }
167
+ set value(v) {
168
+ this.#value = this.isOn ? String(v) : null;
169
+ }
170
+ setValidity = (flags, message, anchor) => {
171
+ // let msg
172
+ // if (message)
173
+ for (const k in flags) {
174
+ const key = k; // ✅ type-safe cast
175
+ if (true !== flags[key])
176
+ continue;
177
+ for (const el of this.#slots?.get('validity-options')?.assignedElements({ flatten: true }) ?? []) {
178
+ if (!el.matches('option'))
179
+ continue;
180
+ const { label, value } = el;
181
+ if (`${kebabCase(key)}` === value) {
182
+ message = label;
183
+ break;
184
+ }
185
+ else if (`${kebabCase(key)}:${message}` === value) {
186
+ message = label;
187
+ break;
188
+ }
189
+ }
190
+ }
191
+ if (!message)
192
+ for (const k in flags) {
193
+ const key = k; // ✅ type-safe cast
194
+ if (true !== flags[key])
195
+ continue;
196
+ message = kebabCase(key);
197
+ break;
198
+ }
199
+ if (devFlags.debug)
200
+ console.debug(`${_a.name} ⚡️ validity-change`);
201
+ return this.#internals.setValidity(flags, this.#customValidity || message, anchor ?? this.#input);
202
+ };
203
+ setCustomValidity = (message) => {
204
+ this.#customValidity = message;
205
+ if (this.#customValidity)
206
+ this.#internals.setValidity({ ...this.#internals.validity, customError: true }, message);
207
+ else
208
+ this.#sendValueToForm(false);
209
+ };
210
+ formAssociatedCallback = (form) => {
211
+ this.#sendValueToForm();
212
+ };
213
+ formDisabledCallback = (disabled) => {
214
+ for (const el of this.#shadowRoot.querySelectorAll('input'))
215
+ el.toggleAttribute('disabled', !disabled);
216
+ };
217
+ formResetCallback = () => {
218
+ this.value = '';
219
+ this.#sendValueToForm();
220
+ };
221
+ }
222
+ _a = ToggleView;
@@ -5,6 +5,10 @@
5
5
  *
6
6
  * @attr {@spacingSet} spacing - The gap between the primary axis
7
7
  *
8
+ * @attr {@alignmentSet} alignment - The cross-axis alignment
9
+ * @attr {@distributionSet} distribution - The main-axis alignment
10
+ * @attr {@placementSet} placement - The main-axis alignment
11
+ *
8
12
  */
9
13
  export declare class VStack extends HTMLElement {
10
14
  constructor();
@@ -6,6 +6,10 @@ import { devFlags } from '../internal/utils';
6
6
  *
7
7
  * @attr {@spacingSet} spacing - The gap between the primary axis
8
8
  *
9
+ * @attr {@alignmentSet} alignment - The cross-axis alignment
10
+ * @attr {@distributionSet} distribution - The main-axis alignment
11
+ * @attr {@placementSet} placement - The main-axis alignment
12
+ *
9
13
  */
10
14
  export class VStack extends HTMLElement {
11
15
  constructor() {