@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.
- package/CHANGELOG.md +5 -0
- package/README.md +25 -30
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/helpers.d.mts.map +1 -1
- package/dist/helpers.mjs +28 -27
- package/dist/helpers.mjs.map +1 -1
- package/dist/ui/component/ibgib-dynamic-component-bases.d.mts +54 -1
- package/dist/ui/component/ibgib-dynamic-component-bases.d.mts.map +1 -1
- package/dist/ui/component/ibgib-dynamic-component-bases.mjs +176 -2
- package/dist/ui/component/ibgib-dynamic-component-bases.mjs.map +1 -1
- package/dist/ui/ui-constants.d.mts +5 -0
- package/dist/ui/ui-constants.d.mts.map +1 -1
- package/dist/ui/ui-constants.mjs +7 -0
- package/dist/ui/ui-constants.mjs.map +1 -1
- package/dist/ui/ui-helpers.d.mts +17 -1
- package/dist/ui/ui-helpers.d.mts.map +1 -1
- package/dist/ui/ui-helpers.mjs +94 -22
- package/dist/ui/ui-helpers.mjs.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/helpers.mts +34 -25
- package/src/ui/component/ARCHITECTURE.md +180 -0
- package/src/ui/component/README.md +201 -0
- package/src/ui/component/ibgib-dynamic-component-bases.mts +225 -2
- package/src/ui/ui-constants.mts +10 -0
- package/src/ui/ui-helpers.mts +137 -23
- package/tools/auto-generated-agent-skills/skills/ibgib-create-component/SKILL.md +9 -5
|
@@ -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
|
-
*
|
|
206
|
+
* Indicates whether the component is currently performing an asynchronous operation.
|
|
205
207
|
*/
|
|
206
|
-
public get isBusy(): boolean { return
|
|
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
|
+
}
|
package/src/ui/ui-constants.mts
CHANGED
|
@@ -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
|
+
|