Skip to content

Commit c97dcb4

Browse files
committed
feat: added typescript package for release
1 parent dbc63f0 commit c97dcb4

122 files changed

Lines changed: 2984 additions & 2731 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/TemplateGenerator/TypeScriptGenerator.php

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -282,23 +282,55 @@ private function getPropertyType(\ReflectionProperty $prop, \ReflectionMethod $g
282282
} elseif ($typeName === 'bool') {
283283
return $allowsNull ? 'boolean | null | undefined' : 'boolean | undefined';
284284
} elseif (str_ends_with($typeName, 'Enum')) {
285-
return $allowsNull ? 'string | boolean | null | undefined' : 'string | boolean | undefined';
285+
$enumClass = basename(str_replace('\\', '/', $typeName));
286+
$enumType = $this->getEnumType($enumClass);
287+
// Check if enum has true/false values, if so, allow boolean
288+
$fullClassName = "Html\\Enum\\{$enumClass}";
289+
if (enum_exists($fullClassName)) {
290+
try {
291+
$cases = $fullClassName::cases();
292+
$values = array_map(fn($case) => $case->value, $cases);
293+
if (in_array('true', $values) && in_array('false', $values)) {
294+
$enumType .= ' | boolean';
295+
}
296+
} catch (\Throwable $e) {
297+
// Ignore
298+
}
299+
}
300+
return $allowsNull ? $enumType . ' | null | undefined' : $enumType . ' | undefined';
286301
} else {
287302
return $allowsNull ? 'string | null | undefined' : 'string | undefined'; // Default fallback
288303
}
289304
} elseif ($type instanceof ReflectionUnionType) {
290305
$types = [];
306+
$allowsNull = false;
291307
foreach ($type->getTypes() as $unionType) {
292308
$unionTypeName = $unionType->getName();
293309
if ($unionTypeName === 'null') {
294-
continue; // Handle nullability separately
310+
$allowsNull = true;
311+
continue;
295312
} elseif (str_ends_with($unionTypeName, 'Enum')) {
296-
$types[] = 'string | boolean | null | undefined';
313+
$enumType = $this->getEnumType(basename(str_replace('\\', '/', $unionTypeName)));
314+
// Check if enum has true/false values, if so, allow boolean
315+
$fullClassName = "Html\\Enum\\{$unionTypeName}";
316+
if (enum_exists($fullClassName)) {
317+
try {
318+
$cases = $fullClassName::cases();
319+
$values = array_map(fn($case) => $case->value, $cases);
320+
if (in_array('true', $values) && in_array('false', $values)) {
321+
$enumType .= ' | boolean';
322+
}
323+
} catch (\Throwable $e) {
324+
// Ignore
325+
}
326+
}
327+
$types[] = $enumType;
297328
} else {
298329
$types[] = $this->getTypeName($unionTypeName);
299330
}
300331
}
301-
return implode(' | ', $types);
332+
$baseType = implode(' | ', $types);
333+
return $allowsNull ? $baseType . ' | null | undefined' : $baseType . ' | undefined';
302334
}
303335
return 'any';
304336
}
@@ -507,7 +539,7 @@ private function buildTypeScriptClass(
507539
$methodName = $this->toPascalCase($propName);
508540
$valueType = $propData['type'];
509541
$ts .= " set{$methodName}(value: {$valueType}): this {\n";
510-
if ($valueType === 'string | boolean | null | undefined') {
542+
if ($propData['isEnum'] ?? false) {
511543
$ts .= " if (value === null || value === undefined) {\n";
512544
$ts .= " this.element.removeAttribute('{$propName}');\n";
513545
$ts .= " } else {\n";

templates/blade/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.2.24",
44
"type": "module",
55
"license": "MIT",
6-
"description": "Fully Type-Safe, WCAG and ARIA compliant, HTML5 components for use in more consistent Blade templates. Has static attribute enum validation.",
6+
"description": "Type-safe, auto-generated Blade templates for all HTML5 elements with full WCAG, ARIA support and validation. Part of Extended HTMLDocument - schema-first from HTML5 schema.",
77
"repository": "https://github.com/vardumper/extended-htmldocument/tree/main/templates/blade",
88
"author": "Erik Poehler",
99
"bugs": {

templates/nextjs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@typesafe-html5/react",
33
"version": "0.2.24",
4-
"description": "Type-safe, framework-agnostic, auto-generated React components for all HTML5 elements with full WCAG, ARIA support and validation.",
4+
"description": "Type-safe, auto-generated React components for all HTML5 elements with full WCAG, ARIA support and validation. Part of Extended HTMLDocument - schema-first from HTML5 schema.",
55
"main": "index.ts",
66
"types": "index.ts",
77
"type": "module",

templates/twig/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.2.24",
44
"type": "module",
55
"license": "MIT",
6-
"description": "Fully Type-Safe, WCAG and ARIA compliant, HTML5 components for use in more consistent Twig templates. Has static attribute (enums) validation.",
6+
"description": "Type-safe, auto-generated Twig templates for all HTML5 elements with full WCAG, ARIA support and validation. Part of Extended HTMLDocument - schema-first from HTML5 schema.",
77
"repository": "https://github.com/vardumper/extended-htmldocument/tree/main/templates/twig",
88
"author": "Erik Poehler",
99
"bugs": {

templates/typescript/.npmignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/node_modules

templates/typescript/README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,43 @@ This directory contains auto-generated TypeScript classes for creating HTML elem
99

1010
## Installation
1111

12-
Copy the generated TypeScript files to your project and import them as needed.
12+
This is a static NPM package, providing just the TypeScript classes.
13+
14+
```bash
15+
npm install @typesafe-html5/typescript
16+
# or
17+
yarn add @typesafe-html5/typescript
18+
# or
19+
pnpm add @typesafe-html5/typescript
20+
```
21+
22+
## Publishing to npm
23+
24+
This package is ready to be published to npm. To publish:
25+
26+
1. Ensure you have an npm account and are logged in (`npm login`)
27+
2. Run `npm publish` from this directory
28+
29+
The package includes:
30+
- All generated TypeScript class files
31+
- Type definitions
32+
- Main entry point (`index.ts`)
33+
- Package metadata and dependencies
34+
35+
## Importing Elements
36+
37+
You can import individual elements from their specific files:
38+
39+
```typescript
40+
import { Input } from './inline/input/input';
41+
import { Button } from './inline/button/button';
42+
```
43+
44+
Or import all elements from the main index file for convenience:
45+
46+
```typescript
47+
import { Input, Button, Div, Form } from './index';
48+
```
1349

1450
## Usage Examples
1551

templates/typescript/block/article/article.ts

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* THIS FILE IS AUTOGENERATED. DO NOT EDIT IT.
33
*
4-
* @generated December 28, 2025 11:40:34
4+
* @generated December 28, 2025 12:26:31
55
* @component Article
66
* @description
77
*/
@@ -18,11 +18,11 @@ export interface ArticleProps {
1818
/**
1919
* Indicates whether assistive technologies should present the entire region as a whole when changes occur.
2020
*/
21-
'aria-atomic'?: string | boolean | null | undefined;
21+
'aria-atomic'?: 'false' | 'true' | boolean | null | undefined;
2222
/**
2323
* The aria-busy attribute is used to indicate whether an element is currently busy or not.
2424
*/
25-
'aria-busy'?: string | boolean | null | undefined;
25+
'aria-busy'?: 'true' | 'false' | boolean | null | undefined;
2626
/**
2727
* Identifies the element(s) whose contents or presence are controlled by this element. Value is a list of IDs separated by a space
2828
*/
@@ -46,35 +46,35 @@ export interface ArticleProps {
4646
/**
4747
* Defines how updates to the element should be announced to screen readers.
4848
*/
49-
'aria-live'?: string | boolean | null | undefined;
49+
'aria-live'?: 'off' | 'polite' | 'assertive' | null | undefined;
5050
/**
5151
* Establishes ownership relationships between elements. Value is a space-separated list of IDs.
5252
*/
5353
'aria-owns'?: string | null | undefined;
5454
/**
5555
* Indicates what content changes should be announced in a live region.
5656
*/
57-
'aria-relevant'?: string | boolean | null | undefined;
57+
'aria-relevant'?: 'additions' | 'removals' | 'text' | 'all' | 'additions text' | null | undefined;
5858
/**
5959
* Provides a human-readable custom role description for assistive technologies.
6060
*/
6161
'aria-roledescription'?: string | null | undefined;
6262
/**
6363
* Represents the autocapitalize behavior of the element
6464
*/
65-
autocapitalize?: string | boolean | null | undefined;
65+
autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | null | undefined;
6666
/**
6767
* Indicates whether the element is hidden
6868
*/
6969
autofocus?: boolean | null | undefined;
7070
/**
7171
* Indicates whether the element can be edited in place
7272
*/
73-
contenteditable?: string | boolean | null | undefined;
73+
contenteditable?: 'true' | 'false' | 'inherit' | boolean | null | undefined;
7474
/**
7575
* Represents the text direction of the element
7676
*/
77-
dir?: string | boolean | null | undefined;
77+
dir?: 'ltr' | 'rtl' | 'auto' | null | undefined;
7878
/**
7979
* Indicates whether the element is draggable
8080
*/
@@ -86,27 +86,27 @@ export interface ArticleProps {
8686
/**
8787
* used to specify the data entry mode for an input. It helps guide on-screen keyboards (especially on mobile devices) to show the appropriate layout for the expected input type
8888
*/
89-
inputmode?: string | boolean | null | undefined;
89+
inputmode?: 'none' | 'text' | 'decimal' | 'numeric' | 'email' | 'tel' | 'url' | 'search' | null | undefined;
9090
/**
9191
* Specifies the primary language for the element's content
9292
*/
9393
lang?: string | null | undefined;
9494
/**
9595
*
9696
*/
97-
popover?: string | boolean | null | undefined;
97+
popover?: 'auto' | 'hint' | 'manual' | null | undefined;
9898
/**
9999
* Defines the semantic purpose of an element for assistive technologies.
100100
*/
101-
role?: string | boolean | null | undefined;
101+
role?: 'alert' | 'application' | 'article' | 'banner' | 'button' | 'checkbox' | 'complementary' | 'contentinfo' | 'dialog' | 'form' | 'grid' | 'group' | 'heading' | 'img' | 'link' | 'list' | 'listbox' | 'listitem' | 'main' | 'menu' | 'menubar' | 'menuitem' | 'navigation' | 'none' | 'presentation' | 'radio' | 'region' | 'search' | 'status' | 'tab' | 'tablist' | 'tabpanel' | 'textbox' | 'toolbar' | 'tooltip' | null | undefined;
102102
/**
103103
* Represents a slot in a shadow DOM
104104
*/
105105
slot?: string | null | undefined;
106106
/**
107107
* Represents the spellchecking behavior of the element
108108
*/
109-
spellcheck?: string | boolean | null | undefined;
109+
spellcheck?: 'true' | 'false' | boolean | null | undefined;
110110
/**
111111
* Represents the CSS inline style of the element
112112
*/
@@ -122,7 +122,7 @@ export interface ArticleProps {
122122
/**
123123
* used to tell user agents whether the content should be translated.
124124
*/
125-
translate?: string | boolean | null | undefined;
125+
translate?: 'yes' | 'no' | null | undefined;
126126
}
127127

128128
/**
@@ -235,7 +235,7 @@ export class Article {
235235
return this;
236236
}
237237

238-
setAriaAtomic(value: string | boolean | null | undefined): this {
238+
setAriaAtomic(value: 'false' | 'true' | boolean | null | undefined): this {
239239
if (value === null || value === undefined) {
240240
this.element.removeAttribute('aria-atomic');
241241
} else {
@@ -244,7 +244,7 @@ export class Article {
244244
return this;
245245
}
246246

247-
setAriaBusy(value: string | boolean | null | undefined): this {
247+
setAriaBusy(value: 'true' | 'false' | boolean | null | undefined): this {
248248
if (value === null || value === undefined) {
249249
this.element.removeAttribute('aria-busy');
250250
} else {
@@ -298,7 +298,7 @@ export class Article {
298298
return this;
299299
}
300300

301-
setAriaLive(value: string | boolean | null | undefined): this {
301+
setAriaLive(value: 'off' | 'polite' | 'assertive' | null | undefined): this {
302302
if (value === null || value === undefined) {
303303
this.element.removeAttribute('aria-live');
304304
} else {
@@ -316,7 +316,7 @@ export class Article {
316316
return this;
317317
}
318318

319-
setAriaRelevant(value: string | boolean | null | undefined): this {
319+
setAriaRelevant(value: 'additions' | 'removals' | 'text' | 'all' | 'additions text' | null | undefined): this {
320320
if (value === null || value === undefined) {
321321
this.element.removeAttribute('aria-relevant');
322322
} else {
@@ -334,7 +334,7 @@ export class Article {
334334
return this;
335335
}
336336

337-
setAutocapitalize(value: string | boolean | null | undefined): this {
337+
setAutocapitalize(value: 'none' | 'sentences' | 'words' | 'characters' | null | undefined): this {
338338
if (value === null || value === undefined) {
339339
this.element.removeAttribute('autocapitalize');
340340
} else {
@@ -352,7 +352,7 @@ export class Article {
352352
return this;
353353
}
354354

355-
setContenteditable(value: string | boolean | null | undefined): this {
355+
setContenteditable(value: 'true' | 'false' | 'inherit' | boolean | null | undefined): this {
356356
if (value === null || value === undefined) {
357357
this.element.removeAttribute('contenteditable');
358358
} else {
@@ -361,7 +361,7 @@ export class Article {
361361
return this;
362362
}
363363

364-
setDir(value: string | boolean | null | undefined): this {
364+
setDir(value: 'ltr' | 'rtl' | 'auto' | null | undefined): this {
365365
if (value === null || value === undefined) {
366366
this.element.removeAttribute('dir');
367367
} else {
@@ -388,7 +388,7 @@ export class Article {
388388
return this;
389389
}
390390

391-
setInputmode(value: string | boolean | null | undefined): this {
391+
setInputmode(value: 'none' | 'text' | 'decimal' | 'numeric' | 'email' | 'tel' | 'url' | 'search' | null | undefined): this {
392392
if (value === null || value === undefined) {
393393
this.element.removeAttribute('inputmode');
394394
} else {
@@ -406,7 +406,7 @@ export class Article {
406406
return this;
407407
}
408408

409-
setPopover(value: string | boolean | null | undefined): this {
409+
setPopover(value: 'auto' | 'hint' | 'manual' | null | undefined): this {
410410
if (value === null || value === undefined) {
411411
this.element.removeAttribute('popover');
412412
} else {
@@ -415,7 +415,7 @@ export class Article {
415415
return this;
416416
}
417417

418-
setRole(value: string | boolean | null | undefined): this {
418+
setRole(value: 'alert' | 'application' | 'article' | 'banner' | 'button' | 'checkbox' | 'complementary' | 'contentinfo' | 'dialog' | 'form' | 'grid' | 'group' | 'heading' | 'img' | 'link' | 'list' | 'listbox' | 'listitem' | 'main' | 'menu' | 'menubar' | 'menuitem' | 'navigation' | 'none' | 'presentation' | 'radio' | 'region' | 'search' | 'status' | 'tab' | 'tablist' | 'tabpanel' | 'textbox' | 'toolbar' | 'tooltip' | null | undefined): this {
419419
if (value === null || value === undefined) {
420420
this.element.removeAttribute('role');
421421
} else {
@@ -433,7 +433,7 @@ export class Article {
433433
return this;
434434
}
435435

436-
setSpellcheck(value: string | boolean | null | undefined): this {
436+
setSpellcheck(value: 'true' | 'false' | boolean | null | undefined): this {
437437
if (value === null || value === undefined) {
438438
this.element.removeAttribute('spellcheck');
439439
} else {
@@ -469,7 +469,7 @@ export class Article {
469469
return this;
470470
}
471471

472-
setTranslate(value: string | boolean | null | undefined): this {
472+
setTranslate(value: 'yes' | 'no' | null | undefined): this {
473473
if (value === null || value === undefined) {
474474
this.element.removeAttribute('translate');
475475
} else {

0 commit comments

Comments
 (0)