@rabassoft/schema-engine-angular-aria 0.1.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.
@@ -0,0 +1,49 @@
1
+ import {
2
+ cpSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from 'node:fs';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+
11
+ const sourceBuildDirectory = dirname(fileURLToPath(import.meta.url));
12
+ const pilotPackage = resolve(sourceBuildDirectory, '..');
13
+ const packageRoots = Object.freeze([
14
+ {
15
+ directory: resolve(sourceBuildDirectory, '../../../core/package'),
16
+ name: '@rabassoft/schema-engine',
17
+ },
18
+ {
19
+ directory: resolve(sourceBuildDirectory, '../../../angular/package'),
20
+ name: '@rabassoft/schema-engine-angular',
21
+ },
22
+ ]);
23
+
24
+ for (const { directory, name } of packageRoots) {
25
+ const manifest = JSON.parse(
26
+ readFileSync(join(directory, 'package.json'), 'utf8'),
27
+ );
28
+ if (manifest.name !== name)
29
+ throw new Error(`Unexpected sibling package: ${name}`);
30
+ const target = join(pilotPackage, 'node_modules', ...name.split('/'));
31
+ rmSync(target, { recursive: true, force: true });
32
+ mkdirSync(target, { recursive: true });
33
+ cpSync(join(directory, 'rebuilt-dist'), join(target, 'dist'), {
34
+ recursive: true,
35
+ });
36
+ writeFileSync(
37
+ join(target, 'package.json'),
38
+ `${JSON.stringify(
39
+ {
40
+ name: manifest.name,
41
+ version: manifest.version,
42
+ type: manifest.type,
43
+ exports: manifest.exports,
44
+ },
45
+ null,
46
+ 2,
47
+ )}\n`,
48
+ );
49
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowUnreachableCode": false,
4
+ "allowUnusedLabels": false,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "exactOptionalPropertyTypes": true,
8
+ "experimentalDecorators": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "lib": ["ES2022", "DOM"],
11
+ "module": "NodeNext",
12
+ "moduleResolution": "NodeNext",
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noImplicitOverride": true,
15
+ "noImplicitReturns": true,
16
+ "noUncheckedIndexedAccess": true,
17
+ "noUnusedLocals": true,
18
+ "noUnusedParameters": true,
19
+ "outDir": "../rebuilt-dist",
20
+ "rootDir": "../src",
21
+ "sourceMap": true,
22
+ "strict": true,
23
+ "target": "ES2022",
24
+ "useDefineForClassFields": false,
25
+ "useUnknownInCatchVariables": true,
26
+ "verbatimModuleSyntax": true
27
+ },
28
+ "angularCompilerOptions": {
29
+ "compilationMode": "partial",
30
+ "strictInjectionParameters": true,
31
+ "strictTemplates": true
32
+ },
33
+ "include": ["../src/**/*.ts"]
34
+ }
package/src/index.ts ADDED
@@ -0,0 +1,254 @@
1
+ // Copyright (C) 2026 Ricardo Rabassó Rodríguez, operating as Rabassoft
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ import {
5
+ ChangeDetectionStrategy,
6
+ Component,
7
+ input,
8
+ makeEnvironmentProviders,
9
+ signal,
10
+ type EnvironmentProviders,
11
+ type OnInit,
12
+ } from '@angular/core';
13
+ import { Tab, TabList, TabPanel, Tabs } from '@angular/aria/tabs';
14
+ import {
15
+ SchemaPresentationEntryOutletComponent,
16
+ SchemaPresentationPanelOutletComponent,
17
+ provideSchemaPresentationContainer,
18
+ type AngularPresentationContainerDefinition,
19
+ type AngularPresentationContainerRegistration,
20
+ type AngularPresentationContainerRenderer,
21
+ type AngularPresentationContainerRenderModel,
22
+ } from '@rabassoft/schema-engine-angular';
23
+
24
+ @Component({
25
+ selector: 'schema-aria-presentation-section',
26
+ standalone: true,
27
+ imports: [SchemaPresentationEntryOutletComponent],
28
+ changeDetection: ChangeDetectionStrategy.OnPush,
29
+ template: `
30
+ @if (section(); as model) {
31
+ <fieldset>
32
+ <legend [id]="model.legendId">{{ model.label }}</legend>
33
+ @for (
34
+ child of model.definition.children;
35
+ track child.kind === 'form-node' ? child.node.key : child.key
36
+ ) {
37
+ <schema-presentation-entry-outlet [entry]="child" />
38
+ }
39
+ </fieldset>
40
+ }
41
+ `,
42
+ })
43
+ class AngularAriaPresentationSectionComponent implements AngularPresentationContainerRenderer {
44
+ readonly presentation =
45
+ input.required<AngularPresentationContainerRenderModel>();
46
+ protected readonly section = () => {
47
+ const model = this.presentation();
48
+ return model.kind === 'section' ? model : undefined;
49
+ };
50
+ }
51
+
52
+ @Component({
53
+ selector: 'schema-aria-presentation-tabs',
54
+ standalone: true,
55
+ imports: [
56
+ Tabs,
57
+ TabList,
58
+ Tab,
59
+ TabPanel,
60
+ SchemaPresentationPanelOutletComponent,
61
+ ],
62
+ changeDetection: ChangeDetectionStrategy.OnPush,
63
+ template: `
64
+ @if (tabs(); as model) {
65
+ <div ngTabs>
66
+ <div
67
+ ngTabList
68
+ orientation="horizontal"
69
+ focusMode="roving"
70
+ selectionMode="follow"
71
+ [wrap]="true"
72
+ [(selectedTab)]="selectedTab"
73
+ [id]="model.tablistId"
74
+ [attr.aria-label]="model.label"
75
+ >
76
+ @for (panel of model.panels; track panel.definition.key) {
77
+ <button
78
+ ngTab
79
+ type="button"
80
+ [id]="panel.tabId"
81
+ [value]="panel.tabpanelId"
82
+ >
83
+ {{ panel.label }}
84
+ </button>
85
+ }
86
+ </div>
87
+ @for (panel of model.panels; track panel.definition.key) {
88
+ <div
89
+ ngTabPanel
90
+ [preserveContent]="true"
91
+ [id]="panel.tabpanelId"
92
+ [value]="panel.tabpanelId"
93
+ [hidden]="selectedTab() !== panel.tabpanelId"
94
+ [attr.inert]="selectedTab() !== panel.tabpanelId ? '' : null"
95
+ >
96
+ <schema-presentation-panel-outlet [panel]="panel.definition" />
97
+ </div>
98
+ }
99
+ </div>
100
+ }
101
+ `,
102
+ })
103
+ class AngularAriaPresentationTabsComponent
104
+ implements AngularPresentationContainerRenderer, OnInit
105
+ {
106
+ readonly presentation =
107
+ input.required<AngularPresentationContainerRenderModel>();
108
+ protected readonly selectedTab = signal<string | undefined>(undefined);
109
+ protected readonly tabs = () => {
110
+ const model = this.presentation();
111
+ return model.kind === 'tabs' ? model : undefined;
112
+ };
113
+
114
+ ngOnInit(): void {
115
+ this.selectedTab.set(this.tabs()?.panels[0]?.tabpanelId);
116
+ }
117
+ }
118
+
119
+ @Component({
120
+ selector: 'schema-aria-presentation-accordion',
121
+ standalone: true,
122
+ imports: [SchemaPresentationPanelOutletComponent],
123
+ changeDetection: ChangeDetectionStrategy.OnPush,
124
+ template: `
125
+ @if (accordion(); as model) {
126
+ <div
127
+ role="group"
128
+ [id]="model.accordionId"
129
+ [attr.aria-label]="model.label"
130
+ >
131
+ @for (
132
+ panel of model.panels;
133
+ track panel.definition.key;
134
+ let index = $index
135
+ ) {
136
+ <button
137
+ type="button"
138
+ [id]="panel.triggerId"
139
+ [attr.aria-expanded]="expanded().has(index)"
140
+ [attr.aria-controls]="panel.regionId"
141
+ (click)="toggle(index)"
142
+ >
143
+ {{ panel.label }}
144
+ </button>
145
+ <div
146
+ role="region"
147
+ [id]="panel.regionId"
148
+ [attr.aria-labelledby]="panel.triggerId"
149
+ [hidden]="!expanded().has(index)"
150
+ [attr.inert]="!expanded().has(index) ? '' : null"
151
+ >
152
+ <schema-presentation-panel-outlet [panel]="panel.definition" />
153
+ </div>
154
+ }
155
+ </div>
156
+ }
157
+ `,
158
+ })
159
+ class AngularAriaPresentationAccordionComponent implements AngularPresentationContainerRenderer {
160
+ readonly presentation =
161
+ input.required<AngularPresentationContainerRenderModel>();
162
+ protected readonly expanded = signal<ReadonlySet<number>>(new Set());
163
+ protected readonly accordion = () => {
164
+ const model = this.presentation();
165
+ return model.kind === 'accordion' ? model : undefined;
166
+ };
167
+
168
+ protected toggle(index: number): void {
169
+ const next = new Set(this.expanded());
170
+ if (next.has(index)) next.delete(index);
171
+ else next.add(index);
172
+ this.expanded.set(next);
173
+ }
174
+ }
175
+
176
+ @Component({
177
+ selector: 'schema-aria-presentation-grid',
178
+ standalone: true,
179
+ imports: [SchemaPresentationEntryOutletComponent],
180
+ changeDetection: ChangeDetectionStrategy.OnPush,
181
+ template: `
182
+ @if (grid(); as model) {
183
+ <div
184
+ class="se-aria-grid"
185
+ role="group"
186
+ [id]="model.gridId"
187
+ [attr.aria-label]="model.label"
188
+ [style.--se-aria-grid-columns]="model.definition.columns"
189
+ >
190
+ @for (item of model.items; track item.definition.key) {
191
+ <div
192
+ class="se-aria-grid-cell"
193
+ [id]="item.cellId"
194
+ [style.--se-aria-grid-span]="item.definition.span"
195
+ >
196
+ <schema-presentation-entry-outlet [entry]="item.definition.child" />
197
+ </div>
198
+ }
199
+ </div>
200
+ }
201
+ `,
202
+ })
203
+ class AngularAriaPresentationGridComponent implements AngularPresentationContainerRenderer {
204
+ readonly presentation =
205
+ input.required<AngularPresentationContainerRenderModel>();
206
+ protected readonly grid = () => {
207
+ const model = this.presentation();
208
+ return model.kind === 'grid' ? model : undefined;
209
+ };
210
+ }
211
+
212
+ const registrations: readonly AngularPresentationContainerRegistration[] =
213
+ Object.freeze([
214
+ registration(
215
+ 'angular-aria-section',
216
+ AngularAriaPresentationSectionComponent,
217
+ 'section',
218
+ ),
219
+ registration(
220
+ 'angular-aria-tabs',
221
+ AngularAriaPresentationTabsComponent,
222
+ 'tabs',
223
+ ),
224
+ registration(
225
+ 'angular-aria-accordion',
226
+ AngularAriaPresentationAccordionComponent,
227
+ 'accordion',
228
+ ),
229
+ registration(
230
+ 'angular-aria-grid',
231
+ AngularAriaPresentationGridComponent,
232
+ 'grid',
233
+ ),
234
+ ]);
235
+
236
+ function registration(
237
+ id: string,
238
+ renderer: AngularPresentationContainerRegistration['renderer'],
239
+ kind: AngularPresentationContainerDefinition['kind'],
240
+ ): AngularPresentationContainerRegistration {
241
+ return Object.freeze({
242
+ id,
243
+ renderer,
244
+ tester: (definition: AngularPresentationContainerDefinition) =>
245
+ definition.kind === kind ? 10 : null,
246
+ priority: 0,
247
+ });
248
+ }
249
+
250
+ export function provideSchemaEngineAngularAriaContainers(): EnvironmentProviders {
251
+ return makeEnvironmentProviders(
252
+ registrations.map((value) => provideSchemaPresentationContainer(value)),
253
+ );
254
+ }
package/styles.css ADDED
@@ -0,0 +1,63 @@
1
+ /* Copyright (C) 2026 Ricardo Rabassó Rodríguez, operating as Rabassoft */
2
+ /* SPDX-License-Identifier: AGPL-3.0-only */
3
+
4
+ schema-aria-presentation-section,
5
+ schema-aria-presentation-tabs,
6
+ schema-aria-presentation-accordion,
7
+ schema-aria-presentation-grid {
8
+ --se-aria-container-surface: Canvas;
9
+ --se-aria-container-text: CanvasText;
10
+ --se-aria-container-border: currentColor;
11
+ --se-aria-container-accent: LinkText;
12
+ --se-aria-container-radius: 0.5rem;
13
+ --se-aria-container-gap: 1rem;
14
+
15
+ display: block;
16
+ color: var(--se-aria-container-text);
17
+ }
18
+
19
+ schema-aria-presentation-section fieldset,
20
+ schema-aria-presentation-tabs [ngtabs],
21
+ schema-aria-presentation-accordion > [role='group'],
22
+ schema-aria-presentation-grid .se-aria-grid {
23
+ padding: var(--se-aria-container-gap);
24
+ border: 1px solid var(--se-aria-container-border);
25
+ border-radius: var(--se-aria-container-radius);
26
+ background: var(--se-aria-container-surface);
27
+ }
28
+
29
+ schema-aria-presentation-tabs [ngtablist] {
30
+ display: flex;
31
+ flex-wrap: wrap;
32
+ gap: var(--se-aria-container-gap);
33
+ }
34
+
35
+ schema-aria-presentation-tabs [ngtab][aria-selected='true'] {
36
+ color: var(--se-aria-container-accent);
37
+ }
38
+
39
+ schema-aria-presentation-accordion > [role='group'] {
40
+ display: grid;
41
+ gap: var(--se-aria-container-gap);
42
+ }
43
+
44
+ schema-aria-presentation-grid .se-aria-grid {
45
+ display: grid;
46
+ grid-template-columns: repeat(var(--se-aria-grid-columns), minmax(0, 1fr));
47
+ gap: var(--se-aria-container-gap);
48
+ }
49
+
50
+ schema-aria-presentation-grid .se-aria-grid-cell {
51
+ min-width: 0;
52
+ grid-column: span var(--se-aria-grid-span);
53
+ }
54
+
55
+ @media (max-width: 40rem) {
56
+ schema-aria-presentation-grid .se-aria-grid {
57
+ grid-template-columns: minmax(0, 1fr);
58
+ }
59
+
60
+ schema-aria-presentation-grid .se-aria-grid-cell {
61
+ grid-column: span 1;
62
+ }
63
+ }