@ibgib/web-gib 0.0.46 → 0.0.48

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.
@@ -0,0 +1,180 @@
1
+ # IbGib Dynamic UI Component Architecture
2
+
3
+ This document describes the design of the `@ibgib` UI microframework in `web-gib` and details the base classes, routing service, proxy-backed model synchronization, and form handling base class.
4
+
5
+ ---
6
+
7
+ ## 📖 Component System Architecture Summary
8
+
9
+ * **Overall Approach**: A timeline-driven UI microframework built on standard HTML5 Web Components (Shadow DOM). Rather than relying on standard local state stores, UI rendering, routing, and lifecycle transitions are powered by the native ibgib protocol and Merkle-DAG content-addressing timelines.
10
+ * **Registry & Routing**: `IbGibComponentService` acts as a central registry. When navigating, the service filters registered `IbGibDynamicComponentMeta` factories by path and regex, executing `fnHandleRoute()` to select and inject the matching component instance.
11
+ * **Backing Model Sync via Proxy**:
12
+ * Components maintain a live binding to their backing data via a **`LiveProxyIbGib`**.
13
+ * When the local space or metaspace registers a new mutation/frame on the timeline, the proxy fires `contextUpdated$` and `newContextChild$` (if it's a parent ibgib with children that were rel8d/unrel8d).
14
+ * The base class subscribes to these streams, automatically updating `this.ibGibAddr` and invoking `renderUI()` to redraw the component.
15
+
16
+ ---
17
+
18
+ ## 🏛️ Core Classes Reference
19
+
20
+ ### 1. `IbGibComponentService`
21
+ A singleton service (`componentSvc`) that acts as a central registry and router for all shell components.
22
+ * **Registration**: Components register their factory metadata blueprints using `registerComponentMeta(meta)`.
23
+ * **Resolution**: When routing, `getComponentInstance()` iterates through registered components, first testing `routeRegExp` and then calling `fnHandleRoute()` for fine-grained content validation.
24
+ * **Injection**: `inject(parentEl, instance)` handles clearing old views, appending the new custom element, and handling injection errors.
25
+
26
+ ### 2. `IbGibDynamicComponentMetaBase`
27
+ The abstract factory blueprint class representing a component type's metadata and routing constraints.
28
+ * **Properties**:
29
+ * `componentName`: The custom HTML tag name (e.g. `keystone-creator`).
30
+ * `routeRegExp`: Prefilter regex matching current route path.
31
+ * `fnHandleRoute`: Detailed route matching lambda.
32
+ * `bootstrapPromise`: Deferment promise coordinating with the global ibgib bootstrap state.
33
+ * **Methods**:
34
+ * `createInstance({ path, ibGibAddr })`: Creates the concrete `HTMLElement` instance.
35
+
36
+ ### 3. `IbGibDynamicComponentInstanceBase`
37
+ The base `HTMLElement` custom element class that coordinates DOM lifecycles and `ibgib` context bindings.
38
+ * **Backing Proxy & Subscription**:
39
+ * `loadIbGib()`: Pulls the initial backing data record from the space, wrapping it in a `LiveProxyIbGib`.
40
+ * `handleContextUpdated()`: Reacts to `contextUpdated$` events, synchronizing `this.ibGibAddr`.
41
+ * `handleNewContextChild()`: Reacts to child timeline changes (e.g., additions or removals of related nodes).
42
+ * **Local Coupling**:
43
+ * `initSettings()`: Manages local-only configurations (like active identities or layout preferences) coupled to a shared timeline using mapping indices without mutating the domain data.
44
+ * **Lifecycle Hooks**:
45
+ * `connectedCallback()`: Attaches the Shadow DOM template, style arrays, and triggers `created()`.
46
+ * `created()`: Abstract hook called after the DOM is ready for component-specific setup and render.
47
+ * `renderUI()`: Triggers UI redrawing, resolving unique color palettes (`getDeterministicColorInfo`) based on the backing `ibgib`'s hash.
48
+ * `disconnectedCallback()` / `disconnected()`: Handles event listener unbinding and proxy resource cleanup when removed from the DOM.
49
+
50
+ ### 4. `IbGibFormInstanceBase`
51
+ The intermediate abstract base class specifically designed to handle interactive form operations, loading states, and validation rules.
52
+ * **Subclass Responsibility**: Inherited by components containing inputs and action buttons (e.g., `KeystoneCreatorComponentInstance`).
53
+ * **Standard Methods**:
54
+ * `setLoading(loading, selector)`: Automatically toggles submit/generate button disabled state and updates text.
55
+ * `setStatus(msg, type, areaId, textId)`: Manages error/success status elements and toggles the `hidden` class.
56
+ * `validateForm(formSelector)`: Inspects all form controls within the shadow root and runs HTML5 Constraint Validation checks.
57
+ * `validateInputPattern(opts)`: Validates a single input/textarea element's value against a custom RegExp pattern, adding/removing the `invalid` CSS class and updating validity messages.
58
+ * `dispatchShellMessage(message, level)`: Dispatches bubbling, composed Custom Events (`ibgib-ui-message`) to notify the parent Shell.
59
+
60
+ ---
61
+
62
+ ## 📋 Implementation Plan: `IbGibFormInstanceBase`
63
+
64
+ ```mermaid
65
+ graph TD
66
+ HTMLElement --> IbGibDynamicComponentInstanceBase
67
+ IbGibDynamicComponentInstanceBase --> IbGibFormInstanceBase
68
+ IbGibFormInstanceBase --> KeystoneCreatorComponentInstance
69
+ ```
70
+
71
+ ### 1. Class Interface Implementation
72
+ Introduce `IbGibFormInstanceBase` inside `libs/web-gib/src/ui/component/ibgib-dynamic-component-bases.mts`:
73
+
74
+ ```typescript
75
+ export abstract class IbGibFormInstanceBase<TIbGib extends IbGib_V1 = IbGib_V1, TElements = any>
76
+ extends IbGibDynamicComponentInstanceBase<TIbGib, TElements> {
77
+
78
+ protected setLoading(loading: boolean, submitBtnSelector = '#btn-submit, #btn-generate'): void {
79
+ if (!this.shadowRoot) return;
80
+ const btn = this.shadowRoot.querySelector(submitBtnSelector) as HTMLButtonElement | null;
81
+ if (btn) {
82
+ btn.disabled = loading;
83
+ // Optionally toggle an attribute or standard class
84
+ if (loading) {
85
+ btn.setAttribute('data-loading', 'true');
86
+ } else {
87
+ btn.removeAttribute('data-loading');
88
+ }
89
+ }
90
+ }
91
+
92
+ protected setStatus(
93
+ msg: string,
94
+ type: 'info' | 'success' | 'error',
95
+ statusAreaId = 'status-area',
96
+ statusTextId = 'status-text'
97
+ ): void {
98
+ if (!this.shadowRoot) return;
99
+ const area = this.shadowRoot.getElementById(statusAreaId);
100
+ const text = this.shadowRoot.getElementById(statusTextId);
101
+
102
+ if (text) {
103
+ text.textContent = msg;
104
+ text.className = `status-msg ${type}`;
105
+ }
106
+ if (area) {
107
+ if (msg) {
108
+ area.classList.remove('hidden');
109
+ } else {
110
+ area.classList.add('hidden');
111
+ }
112
+ }
113
+ }
114
+
115
+ protected dispatchShellMessage(message: string, level: 'info' | 'success' | 'warning' | 'error'): void {
116
+ this.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
117
+ detail: { message, level, source: this.tagName.toLowerCase() },
118
+ bubbles: true,
119
+ composed: true
120
+ }));
121
+ }
122
+
123
+ protected validateForm(formSelector = 'form'): boolean {
124
+ if (!this.shadowRoot) return true;
125
+ const form = this.shadowRoot.querySelector(formSelector) as HTMLFormElement | null;
126
+ if (form) {
127
+ return form.checkValidity();
128
+ }
129
+
130
+ // Fallback: check all individual inputs in shadowRoot
131
+ const inputs = this.shadowRoot.querySelectorAll('input, select, textarea');
132
+ let isValid = true;
133
+ for (const input of Array.from(inputs)) {
134
+ const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
135
+ if (!el.checkValidity()) {
136
+ isValid = false;
137
+ el.reportValidity();
138
+ break; // Stop on first invalid field
139
+ }
140
+ }
141
+ return isValid;
142
+ }
143
+
144
+ protected validateInputPattern({
145
+ input,
146
+ pattern,
147
+ errorMessage,
148
+ required = true,
149
+ }: {
150
+ input: HTMLInputElement | HTMLTextAreaElement | null;
151
+ pattern: RegExp;
152
+ errorMessage: string;
153
+ required?: boolean;
154
+ }): boolean {
155
+ if (!input) return true;
156
+ const val = input.value.trim();
157
+
158
+ if (!val && !required) {
159
+ input.classList.remove('invalid');
160
+ input.setCustomValidity('');
161
+ return true;
162
+ }
163
+
164
+ const isValid = pattern.test(val);
165
+ if (!isValid) {
166
+ input.classList.add('invalid');
167
+ input.setCustomValidity(errorMessage);
168
+ input.reportValidity();
169
+ } else {
170
+ input.classList.remove('invalid');
171
+ input.setCustomValidity('');
172
+ }
173
+ return isValid;
174
+ }
175
+ }
176
+ ```
177
+
178
+ ### 2. Integration
179
+ * Refactor `KeystoneCreatorComponentInstance` in `space-gib` to extend `IbGibFormInstanceBase` instead of `IbGibDynamicComponentInstanceBase`.
180
+ * Replace its local duplicate validation and loading methods with inherited calls.
@@ -0,0 +1,201 @@
1
+ # Custom @ibgib UI Component Framework
2
+
3
+ This directory houses the custom `@ibgib` UI microframework. Rather than using conventional state-binding engines, this system leverages the native ibgib protocol and its Merkle-DAG content-addressing structure to orchestrate a unique, timeline-driven reactivity. Component instances are implemented as native HTML5 Web Components (Shadow DOM) and bind directly to evolutionary `ibGib` timelines, resolving, mutating, and updating state in sync with the DLT storage.
4
+
5
+ For the internal architectural details and class reference, see [ARCHITECTURE.md](file:///c:/Users/billm/antigravity/ibgib/libs/web-gib/src/ui/component/ARCHITECTURE.md).
6
+
7
+ For a helper agentic skill, see the [ibgib-create-component folder](file:///c:\Users\billm\antigravity\ibgib\.agents\skills\ibgib-create-component)
8
+
9
+ ---
10
+
11
+ ## 💡 Overview
12
+
13
+ Unlike standard frameworks (React, Vue, etc.) that bind to a local central store, `@ibgib` components are linked to a backing `ibgib` timeline.
14
+ - The component uses the timeline's genesis address (its immutable `tjpGib`) as its identifier.
15
+ - The component subscribes to the metaspace's local pubsub/event bus.
16
+ - When mutations or new frames occur on the timeline, the component automatically evolves, synchronizes its state, and redraws itself.
17
+
18
+ The protocol itself is a universe-sized address spacetime that allows truly distributed data to drive the reactivity.
19
+
20
+ ### The Component Framework Parts
21
+
22
+ 1. **`IbGibComponentService` (Registry & Router)**: A central registry and router singleton. When navigating, it matches route pathing, selects the appropriate component factory meta, and injects the instantiated element into the target DOM.
23
+ 2. **`IbGibDynamicComponentMetaBase` (Factory Meta)**: A metadata blueprint specifying the component's HTML tag name, routing regex, and containing the `createInstance` factory method.
24
+ 3. **`IbGibDynamicComponentInstanceBase` (UI Component)**: The concrete custom element inheriting from `HTMLElement` (i.e. web component). It wraps the backing data in a `LiveProxyIbGib` and automatically triggers lifecycle/render hooks on timeline updates.
25
+ 4. **`IbGibFormInstanceBase` (Form Handler)**: An intermediate base class extending the instance base, adding status reporting, button load toggling, and validation for behavior specific to forms.
26
+
27
+ ---
28
+
29
+ ## 🛠️ How to Create an IbGib UI Component
30
+
31
+ ### 1. Registering your Component Metadata
32
+ To register your component with the shell router, extend `IbGibDynamicComponentMetaBase` and call `componentSvc.registerComponentMeta(new MyComponentMeta())` during your application bootstrap:
33
+
34
+ ```typescript
35
+ import { IbGibDynamicComponentMetaBase } from '@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs';
36
+ import { componentSvc } from '@ibgib/web-gib/dist/ui/component/component-service.mjs';
37
+
38
+ export const MY_COMPONENT_NAME = 'my-custom-component';
39
+
40
+ export class MyComponentMeta extends IbGibDynamicComponentMetaBase {
41
+ routeRegExp = new RegExp(`^${MY_COMPONENT_NAME}$`);
42
+ componentName = MY_COMPONENT_NAME;
43
+
44
+ createInstance(opts) {
45
+ return document.createElement(this.componentName);
46
+ }
47
+ }
48
+
49
+ // Register with central service
50
+ componentSvc.registerComponentMeta(new MyComponentMeta());
51
+ ```
52
+
53
+ ### 2. Implementing the Component Instance
54
+ Your component class extends `IbGibDynamicComponentInstanceBase`. You override `created()` for DOM initialization and `renderUI()` for drawing data:
55
+
56
+ ```typescript
57
+ import { IbGibDynamicComponentInstanceBase } from '@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs';
58
+ import thisHtml from './my-component.html';
59
+ import thisCss from './my-component.css';
60
+
61
+ export class MyComponentInstance extends IbGibDynamicComponentInstanceBase {
62
+ protected lc = '[MyComponentInstance]';
63
+
64
+ // Specify the HTML template and stylesheets
65
+ protected get html() { return thisHtml; }
66
+ protected get css() { return [thisCss]; }
67
+
68
+ /** Called once when custom element is connected to the DOM */
69
+ override async created(): Promise<void> {
70
+ // Query elements within this.shadowRoot
71
+ this.elements.myValueEl = this.shadowRoot.getElementById('value-display');
72
+
73
+ // Load backing model if ibGibAddr was passed
74
+ if (this.ibGibAddr && !this.ibGib) {
75
+ await this.loadIbGib();
76
+ }
77
+ await this.renderUI();
78
+ }
79
+
80
+ /** Called initially, and automatically on backing ibgib updates */
81
+ override async renderUI(): Promise<void> {
82
+ if (!this.ibGib) return;
83
+
84
+ // Dynamically update elements
85
+ this.elements.myValueEl.textContent = this.ibGib.data?.value ?? '';
86
+ }
87
+ }
88
+
89
+ customElements.define(MY_COMPONENT_NAME, MyComponentInstance);
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 📝 Form Handling & Validation UI
95
+
96
+ When building forms, your component instance should extend **`IbGibFormInstanceBase`** instead of `IbGibDynamicComponentInstanceBase`. This adds powerful built-in validation and status tracking.
97
+
98
+ ### Form Validation API Reference
99
+
100
+ * **`validateForm(formSelector?: string): boolean`**
101
+ Checks standard HTML5 constraint validation on all inputs in the form (or shadow root). Reports validity and returns `true` if valid, `false` otherwise.
102
+ * **`validateInputPattern(opts: ValidateInputPatternOpts): boolean`**
103
+ Validates a single input's value against a JS `RegExp`. If invalid, it adds the `.invalid` CSS class to the input element, sets custom error validity messages, and calls `reportValidity()` to trigger standard browser tooltip notifications.
104
+ * **`setLoading(loading: boolean, submitBtnSelector?: string): void`**
105
+ Toggles the `disabled` state on the submit/action button, adding a `data-loading` attribute.
106
+ * **`setStatus(msg: string, type: 'info' | 'success' | 'error', statusAreaId?: string, statusTextId?: string): void`**
107
+ Displays success/error banners on the form page, removing the `.hidden` class from the status area wrapper.
108
+
109
+ ### Validation Example Flow
110
+
111
+ Here is how to structure form validation and clear state flags as the user types:
112
+
113
+ #### HTML Template (`my-form.html`)
114
+ ```html
115
+ <div id="container">
116
+ <div id="status-area" class="hidden">
117
+ <p id="status-text"></p>
118
+ </div>
119
+
120
+ <form id="my-form">
121
+ <label for="input-username">Username</label>
122
+ <input type="text" id="input-username" required />
123
+
124
+ <button type="submit" id="btn-submit">Submit</button>
125
+ </form>
126
+ </div>
127
+ ```
128
+
129
+ #### CSS Stylesheet (`my-form.css`)
130
+ ```css
131
+ /* Styling input fields marked as invalid */
132
+ input.invalid {
133
+ border: 2px solid var(--clr-danger, #ff4444);
134
+ box-shadow: 0 0 5px rgba(255, 68, 68, 0.5);
135
+ outline: none;
136
+ }
137
+
138
+ .status-msg.error {
139
+ color: var(--clr-danger, #ff4444);
140
+ font-weight: 500;
141
+ }
142
+ .status-msg.success {
143
+ color: var(--clr-success, #00c851);
144
+ }
145
+ ```
146
+
147
+ #### TypeScript Implementation (`my-form.mts`)
148
+ ```typescript
149
+ import { IbGibFormInstanceBase } from '@ibgib/web-gib/dist/ui/component/ibgib-dynamic-component-bases.mjs';
150
+ import { KEYSTONE_USERNAME_REGEXP } from '@ibgib/core-gib/dist/keystone/keystone-constants.mjs';
151
+
152
+ export class MyFormInstance extends IbGibFormInstanceBase {
153
+
154
+ override async created(): Promise<void> {
155
+ this.elements.inputUsername = this.shadowRoot.getElementById('input-username');
156
+ this.elements.btnSubmit = this.shadowRoot.getElementById('btn-submit');
157
+
158
+ this.initHandlers();
159
+ }
160
+
161
+ private initHandlers() {
162
+ this.shadowRoot.getElementById('my-form').addEventListener('submit', (e) => {
163
+ e.preventDefault();
164
+ this.handleSubmit();
165
+ });
166
+
167
+ // CRITICAL: Clear the invalid CSS class as the user starts typing
168
+ this.elements.inputUsername.addEventListener('input', () => {
169
+ this.elements.inputUsername.classList.remove('invalid');
170
+ });
171
+ }
172
+
173
+ private async handleSubmit() {
174
+ // 1. Perform custom Regexp validation
175
+ const isValidUsername = this.validateInputPattern({
176
+ input: this.elements.inputUsername,
177
+ pattern: KEYSTONE_USERNAME_REGEXP,
178
+ errorMessage: "Username must be 1-63 characters (alphanumeric, dots, hyphens, underscores).",
179
+ required: true
180
+ });
181
+
182
+ if (!isValidUsername) {
183
+ this.setStatus("Please correct the invalid fields.", "error");
184
+ return;
185
+ }
186
+
187
+ // 2. Toggle Loading State
188
+ this.setLoading(true, '#btn-submit');
189
+ this.setStatus("Submitting username...", "info");
190
+
191
+ try {
192
+ // ... perform business logic / space evolution ...
193
+ this.setStatus("Form successfully submitted!", "success");
194
+ } catch (err) {
195
+ this.setStatus(`Error occurred: ${err.message}`, "error");
196
+ } finally {
197
+ this.setLoading(false, '#btn-submit');
198
+ }
199
+ }
200
+ }
201
+ ```
@@ -51,6 +51,7 @@ import { getAgentForDomainIbGib } from "../../witness/agent/agent-helpers.mjs";
51
51
  import { createSettings, getSectionName, getSettingsScope, getSettingsSection } from "../../common/settings/settings-helpers.mjs";
52
52
  import { IbGibSettings, SettingsIbGib_V1, SettingsWithTabs } from "../../common/settings/settings-types.mjs";
53
53
  import { SettingsType } from "../../common/settings/settings-constants.mjs";
54
+ import { EVENT_IBGIB_UI_BUSY, EVENT_IBGIB_UI_MESSAGE } from "../ui-constants.mjs";
54
55
 
55
56
  const logalot = GLOBAL_LOG_A_LOT;
56
57
 
@@ -200,10 +201,51 @@ export abstract class IbGibDynamicComponentInstanceBase<TIbGib extends IbGib_V1
200
201
  */
201
202
  protected connectedCount = 0;
202
203
 
204
+ protected _isBusy: boolean = false;
203
205
  /**
204
- * override this as needed
206
+ * Indicates whether the component is currently performing an asynchronous operation.
205
207
  */
206
- public get isBusy(): boolean { return false; }
208
+ public get isBusy(): boolean { return this._isBusy; }
209
+
210
+ /**
211
+ * Updates the component's busy state, disabling elements and triggering events.
212
+ * Dispatches `EVENT_IBGIB_UI_BUSY` locally on the component and globally on the window.
213
+ */
214
+ protected showBusy({
215
+ isBusy,
216
+ title,
217
+ msg,
218
+ localOnly,
219
+ animationEmoji,
220
+ }: {
221
+ isBusy: boolean;
222
+ title?: string;
223
+ msg?: string;
224
+ localOnly?: boolean;
225
+ animationEmoji?: string;
226
+ }): void {
227
+ const lc = `${this.lc}[${this.showBusy.name}]`;
228
+ try {
229
+ this._isBusy = isBusy;
230
+
231
+ if (localOnly) {
232
+ // Dispatch locally on 'this' (bubbles so parent components can listen)
233
+ this.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_BUSY, {
234
+ detail: { isBusy, title, msg, animationEmoji },
235
+ bubbles: true,
236
+ composed: true
237
+ }));
238
+ } else {
239
+ // Unless localOnly, dispatch directly on window (so shell receives it even if unmounted)
240
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_BUSY, {
241
+ detail: { isBusy, title, msg, animationEmoji }
242
+ }));
243
+ }
244
+ } catch (error) {
245
+ console.error(`${lc} ${extractErrorMsg(error)}`);
246
+ throw error;
247
+ }
248
+ }
207
249
 
208
250
  protected async getMetaspace(): Promise<MetaspaceService> {
209
251
  const lc = `${this.lc}[${this.getMetaspace.name}]`;
@@ -1369,3 +1411,184 @@ export abstract class IbGibDynamicComponentInstanceBase_ParentOfTabs<TSettings e
1369
1411
  }
1370
1412
  }
1371
1413
  }
1414
+
1415
+ /**
1416
+ * Intermediate abstract base class designed for dynamic interactive form elements.
1417
+ * Provides standard wrappers for constraint validation, busy states, status reporting,
1418
+ * and boundary-crossing shell notification events.
1419
+ */
1420
+ export abstract class IbGibFormInstanceBase<TIbGib extends IbGib_V1 = IbGib_V1, TElements = any>
1421
+ extends IbGibDynamicComponentInstanceBase<TIbGib, TElements> {
1422
+
1423
+ /**
1424
+ * Updates the form's submit/generate button visual loading state.
1425
+ * Disables the button, sets custom loading text, and marks it with data attributes
1426
+ * during loading; restores the original text and removes attributes when complete.
1427
+ */
1428
+ protected setLoading(
1429
+ loading: boolean,
1430
+ submitBtnSelector = '#btn-submit, #btn-generate',
1431
+ loadingText?: string
1432
+ ): void {
1433
+ if (!this.shadowRoot) return;
1434
+ const btn = this.shadowRoot.querySelector(submitBtnSelector) as HTMLButtonElement | null;
1435
+ if (btn) {
1436
+ btn.disabled = loading;
1437
+ if (loading) {
1438
+ btn.setAttribute('data-loading', 'true');
1439
+ if (loadingText) {
1440
+ btn.setAttribute('data-original-text', btn.textContent || '');
1441
+ btn.textContent = loadingText;
1442
+ }
1443
+ } else {
1444
+ btn.removeAttribute('data-loading');
1445
+ const orig = btn.getAttribute('data-original-text');
1446
+ if (orig !== null) {
1447
+ btn.textContent = orig;
1448
+ btn.removeAttribute('data-original-text');
1449
+ }
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ protected setStatus(
1455
+ msg: string,
1456
+ type: 'info' | 'success' | 'error',
1457
+ statusAreaId = 'status-area',
1458
+ statusTextId = 'status-text'
1459
+ ): void {
1460
+ if (!this.shadowRoot) return;
1461
+ const area = this.shadowRoot.getElementById(statusAreaId);
1462
+ const text = this.shadowRoot.getElementById(statusTextId);
1463
+
1464
+ if (text) {
1465
+ text.textContent = msg;
1466
+ text.className = `status-msg ${type}`;
1467
+ }
1468
+ if (area) {
1469
+ if (msg) {
1470
+ area.classList.remove('hidden');
1471
+ } else {
1472
+ area.classList.add('hidden');
1473
+ }
1474
+ }
1475
+ }
1476
+
1477
+ protected dispatchShellMessage(message: string, level: 'info' | 'success' | 'warning' | 'error'): void {
1478
+ this.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, {
1479
+ detail: { message, level, source: this.tagName.toLowerCase() },
1480
+ bubbles: true,
1481
+ composed: true
1482
+ }));
1483
+ }
1484
+
1485
+ protected validateForm(formSelector = 'form'): boolean {
1486
+ if (!this.shadowRoot) return true;
1487
+ const form = this.shadowRoot.querySelector(formSelector) as HTMLFormElement | null;
1488
+ if (form) {
1489
+ return form.checkValidity();
1490
+ }
1491
+
1492
+ // Fallback: check all individual inputs in shadowRoot
1493
+ const inputs = this.shadowRoot.querySelectorAll('input, select, textarea');
1494
+ let isValid = true;
1495
+ for (const input of Array.from(inputs)) {
1496
+ const el = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
1497
+ if (!el.checkValidity()) {
1498
+ isValid = false;
1499
+ el.reportValidity();
1500
+ break; // Stop on first invalid field
1501
+ }
1502
+ }
1503
+ return isValid;
1504
+ }
1505
+
1506
+ /**
1507
+ * Validates a given input element's value against a RegExp pattern.
1508
+ * Marks the input element with the 'invalid' class and sets a custom validity message if invalid.
1509
+ */
1510
+ protected validateInputPattern({
1511
+ input,
1512
+ pattern,
1513
+ errorMessage,
1514
+ required = true,
1515
+ }: {
1516
+ input: HTMLInputElement | HTMLTextAreaElement | null;
1517
+ pattern: RegExp;
1518
+ errorMessage: string;
1519
+ required?: boolean;
1520
+ }): boolean {
1521
+ if (!input) return true;
1522
+ const val = input.value.trim();
1523
+
1524
+ // If empty and not required, it is valid
1525
+ if (!val && !required) {
1526
+ input.classList.remove('invalid');
1527
+ input.setCustomValidity('');
1528
+ return true;
1529
+ }
1530
+
1531
+ const isValid = pattern.test(val);
1532
+ if (!isValid) {
1533
+ input.classList.add('invalid');
1534
+ input.setCustomValidity(errorMessage);
1535
+ input.reportValidity();
1536
+ } else {
1537
+ input.classList.remove('invalid');
1538
+ input.setCustomValidity('');
1539
+ }
1540
+ return isValid;
1541
+ }
1542
+
1543
+ /**
1544
+ * Overrides base component showBusy to handle form-specific side-effects
1545
+ * (e.g. disabling form inputs, setting submit button loading state)
1546
+ * before delegating to the base class event dispatchers.
1547
+ */
1548
+ protected override showBusy({
1549
+ isBusy,
1550
+ title,
1551
+ msg,
1552
+ localOnly,
1553
+ submitBtnSelector,
1554
+ loadingText,
1555
+ animationEmoji,
1556
+ }: {
1557
+ isBusy: boolean;
1558
+ title?: string;
1559
+ msg?: string;
1560
+ localOnly?: boolean;
1561
+ submitBtnSelector?: string;
1562
+ loadingText?: string;
1563
+ animationEmoji?: string;
1564
+ }): void {
1565
+ const lc = `${this.lc}[${this.showBusy.name}]`;
1566
+ try {
1567
+ // Apply local form-specific side effects
1568
+ this.setLoading(isBusy, submitBtnSelector, loadingText);
1569
+ this.toggleAllInputsDisabled(isBusy);
1570
+
1571
+ // Delegate core state update and local/global event dispatching to base class
1572
+ super.showBusy({
1573
+ isBusy,
1574
+ title,
1575
+ msg,
1576
+ localOnly,
1577
+ animationEmoji
1578
+ });
1579
+ } catch (error) {
1580
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1581
+ throw error;
1582
+ }
1583
+ }
1584
+
1585
+ private toggleAllInputsDisabled(disabled: boolean): void {
1586
+ if (!this.shadowRoot) { return; }
1587
+ const inputs = this.shadowRoot.querySelectorAll('input, select, textarea, button');
1588
+ inputs.forEach(input => {
1589
+ // Don't disable the submit/generate button since setLoading handles its text/state
1590
+ if (input.id === 'btn-submit' || input.id === 'btn-generate') { return; }
1591
+ (input as HTMLInputElement | HTMLButtonElement).disabled = disabled;
1592
+ });
1593
+ }
1594
+ }
@@ -99,3 +99,13 @@ export const VALID_CSS_VARIABLES = [
99
99
  * going to use this for storing/restoring themes
100
100
  */
101
101
  export const UI_THEME_INFO_KEY = 'ui-theme-info';
102
+
103
+ // #region Custom Events
104
+ export const EVENT_IBGIB_SHELL_READY = 'ibgib-shell-ready';
105
+ export const EVENT_IBGIB_UI_BUSY = 'ibgib-ui-busy';
106
+ export const EVENT_IBGIB_UI_MESSAGE = 'ibgib-ui-message';
107
+ export const EVENT_IBGIB_IDENTITY_REQUEST_CHANGE = 'ibgib-identity-request-change';
108
+ export const EVENT_IBGIB_IDENTITY_CHANGED = 'ibgib-identity-changed';
109
+ // #endregion Custom Events
110
+
111
+