@neumorphism-ui/elements 0.0.0-stage → 1.0.0-next.1

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +63 -2
  3. package/cdn/neu.min.js +205 -0
  4. package/custom-elements.json +1892 -0
  5. package/dist/all.d.ts +12 -0
  6. package/dist/all.js +12 -0
  7. package/dist/avatar.d.ts +3 -0
  8. package/dist/avatar.js +4 -0
  9. package/dist/badge.d.ts +3 -0
  10. package/dist/badge.js +4 -0
  11. package/dist/button-group.d.ts +3 -0
  12. package/dist/button-group.js +4 -0
  13. package/dist/button.d.ts +3 -0
  14. package/dist/button.js +4 -0
  15. package/dist/components/avatar/avatar.d.ts +38 -0
  16. package/dist/components/avatar/avatar.def.d.ts +24 -0
  17. package/dist/components/avatar/avatar.def.js +33 -0
  18. package/dist/components/avatar/avatar.js +58 -0
  19. package/dist/components/avatar/avatar.styles.d.ts +1 -0
  20. package/dist/components/avatar/avatar.styles.js +13 -0
  21. package/dist/components/badge/badge.d.ts +17 -0
  22. package/dist/components/badge/badge.def.d.ts +12 -0
  23. package/dist/components/badge/badge.def.js +29 -0
  24. package/dist/components/badge/badge.js +5 -0
  25. package/dist/components/badge/badge.styles.d.ts +1 -0
  26. package/dist/components/badge/badge.styles.js +10 -0
  27. package/dist/components/button/button.d.ts +25 -0
  28. package/dist/components/button/button.def.d.ts +28 -0
  29. package/dist/components/button/button.def.js +45 -0
  30. package/dist/components/button/button.js +87 -0
  31. package/dist/components/button/button.styles.d.ts +2 -0
  32. package/dist/components/button/button.styles.js +43 -0
  33. package/dist/components/button-group/button-group.d.ts +19 -0
  34. package/dist/components/button-group/button-group.def.d.ts +12 -0
  35. package/dist/components/button-group/button-group.def.js +25 -0
  36. package/dist/components/button-group/button-group.js +13 -0
  37. package/dist/components/button-group/button-group.styles.d.ts +7 -0
  38. package/dist/components/button-group/button-group.styles.js +19 -0
  39. package/dist/components/divider/divider.d.ts +14 -0
  40. package/dist/components/divider/divider.def.d.ts +8 -0
  41. package/dist/components/divider/divider.def.js +24 -0
  42. package/dist/components/divider/divider.js +13 -0
  43. package/dist/components/divider/divider.styles.d.ts +1 -0
  44. package/dist/components/divider/divider.styles.js +7 -0
  45. package/dist/components/icon/icon-drawer.d.ts +12 -0
  46. package/dist/components/icon/icon-drawer.js +38 -0
  47. package/dist/components/icon/icon-loader.d.ts +5 -0
  48. package/dist/components/icon/icon-loader.js +102 -0
  49. package/dist/components/icon/icon-registry.d.ts +11 -0
  50. package/dist/components/icon/icon-registry.js +26 -0
  51. package/dist/components/icon/icon.d.ts +25 -0
  52. package/dist/components/icon/icon.def.d.ts +15 -0
  53. package/dist/components/icon/icon.def.js +24 -0
  54. package/dist/components/icon/icon.js +23 -0
  55. package/dist/components/icon/icon.styles.d.ts +1 -0
  56. package/dist/components/icon/icon.styles.js +7 -0
  57. package/dist/components/icon/system-icons.d.ts +2 -0
  58. package/dist/components/icon/system-icons.js +17 -0
  59. package/dist/components/icon-button/icon-button.d.ts +51 -0
  60. package/dist/components/icon-button/icon-button.def.d.ts +40 -0
  61. package/dist/components/icon-button/icon-button.def.js +44 -0
  62. package/dist/components/icon-button/icon-button.js +21 -0
  63. package/dist/components/icon-button/icon-button.styles.d.ts +1 -0
  64. package/dist/components/icon-button/icon-button.styles.js +12 -0
  65. package/dist/components/spinner/spinner.d.ts +19 -0
  66. package/dist/components/spinner/spinner.def.d.ts +12 -0
  67. package/dist/components/spinner/spinner.def.js +25 -0
  68. package/dist/components/spinner/spinner.js +17 -0
  69. package/dist/components/spinner/spinner.styles.d.ts +1 -0
  70. package/dist/components/spinner/spinner.styles.js +13 -0
  71. package/dist/components/surface/surface.d.ts +33 -0
  72. package/dist/components/surface/surface.def.d.ts +26 -0
  73. package/dist/components/surface/surface.def.js +27 -0
  74. package/dist/components/surface/surface.js +5 -0
  75. package/dist/components/surface/surface.styles.d.ts +1 -0
  76. package/dist/components/surface/surface.styles.js +17 -0
  77. package/dist/components/switch/switch.d.ts +60 -0
  78. package/dist/components/switch/switch.def.d.ts +28 -0
  79. package/dist/components/switch/switch.def.js +33 -0
  80. package/dist/components/switch/switch.js +125 -0
  81. package/dist/components/switch/switch.styles.d.ts +1 -0
  82. package/dist/components/switch/switch.styles.js +29 -0
  83. package/dist/components/text-field/implicit-submit.d.ts +8 -0
  84. package/dist/components/text-field/implicit-submit.js +42 -0
  85. package/dist/components/text-field/text-field.d.ts +116 -0
  86. package/dist/components/text-field/text-field.def.d.ts +143 -0
  87. package/dist/components/text-field/text-field.def.js +63 -0
  88. package/dist/components/text-field/text-field.js +207 -0
  89. package/dist/components/text-field/text-field.styles.d.ts +1 -0
  90. package/dist/components/text-field/text-field.styles.js +39 -0
  91. package/dist/core/define.d.ts +33 -0
  92. package/dist/core/define.js +15 -0
  93. package/dist/core/dom.d.ts +21 -0
  94. package/dist/core/dom.js +51 -0
  95. package/dist/core/form-control.d.ts +35 -0
  96. package/dist/core/form-control.js +128 -0
  97. package/dist/core/html.d.ts +13 -0
  98. package/dist/core/html.js +35 -0
  99. package/dist/core/neu-element.d.ts +87 -0
  100. package/dist/core/neu-element.js +285 -0
  101. package/dist/core/props.d.ts +44 -0
  102. package/dist/core/props.js +52 -0
  103. package/dist/core/styles.d.ts +8 -0
  104. package/dist/core/styles.js +40 -0
  105. package/dist/core/validation.d.ts +4 -0
  106. package/dist/core/validation.js +26 -0
  107. package/dist/divider.d.ts +3 -0
  108. package/dist/divider.js +4 -0
  109. package/dist/icon-button.d.ts +3 -0
  110. package/dist/icon-button.js +4 -0
  111. package/dist/icon.d.ts +3 -0
  112. package/dist/icon.js +4 -0
  113. package/dist/icons.d.ts +2 -0
  114. package/dist/icons.js +1 -0
  115. package/dist/spinner.d.ts +3 -0
  116. package/dist/spinner.js +4 -0
  117. package/dist/ssr.d.ts +11 -0
  118. package/dist/ssr.js +41 -0
  119. package/dist/surface.d.ts +3 -0
  120. package/dist/surface.js +4 -0
  121. package/dist/switch.d.ts +3 -0
  122. package/dist/switch.js +4 -0
  123. package/dist/text-field.d.ts +3 -0
  124. package/dist/text-field.js +4 -0
  125. package/package.json +104 -3
@@ -0,0 +1,13 @@
1
+ import { NeuElement } from '../../core/neu-element.js';
2
+ import { buttonGroupDef } from './button-group.def.js';
3
+ export class NeuButtonGroup extends NeuElement {
4
+ static def = buttonGroupDef;
5
+ constructor() {
6
+ super();
7
+ this.internals.role = 'group';
8
+ }
9
+ update(changed) {
10
+ if (changed.has('label'))
11
+ this.internals.ariaLabel = String(this.props.label) || null;
12
+ }
13
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Merging is pure CSS: the group sets --_neu-grp-* on its slotted buttons, which read them. The
3
+ * outer corners go to the first and last rendered children. Slotted buttons become flex boxes
4
+ * whose base grows, so a stretched button is filled edge to edge (this outer ::slotted rule wins
5
+ * over the button's own :host display). Both rules skip the same children that never render.
6
+ */
7
+ export declare const buttonGroupStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: inline-flex; vertical-align: middle; }\n.base { display: inline-flex; border-radius: var(--_neu-r); box-shadow: var(--_neu-raised); }\n:host([orientation='vertical']) .base { flex-direction: column; }\n::slotted(*) { --_neu-grp-shadow: 0 0 transparent; --_neu-grp-ss: 0; --_neu-grp-se: 0; --_neu-grp-es: 0; --_neu-grp-ee: 0; }\n::slotted(:not([hidden], template, script, style)) { display: flex; }\n::slotted(:focus-visible) { position: relative; z-index: 1; }\n:host(:not([orientation='vertical'])) ::slotted(:nth-child(1 of :not([hidden], template, script, style))) { --_neu-grp-ss: var(--_neu-r); --_neu-grp-es: var(--_neu-r); }\n:host(:not([orientation='vertical'])) ::slotted(:nth-last-child(1 of :not([hidden], template, script, style))) { --_neu-grp-se: var(--_neu-r); --_neu-grp-ee: var(--_neu-r); }\n:host([orientation='vertical']) ::slotted(:nth-child(1 of :not([hidden], template, script, style))) { --_neu-grp-ss: var(--_neu-r); --_neu-grp-se: var(--_neu-r); }\n:host([orientation='vertical']) ::slotted(:nth-last-child(1 of :not([hidden], template, script, style))) { --_neu-grp-es: var(--_neu-r); --_neu-grp-ee: var(--_neu-r); }\n";
@@ -0,0 +1,19 @@
1
+ import { hostTheme } from '../../core/styles.js';
2
+ /**
3
+ * Merging is pure CSS: the group sets --_neu-grp-* on its slotted buttons, which read them. The
4
+ * outer corners go to the first and last rendered children. Slotted buttons become flex boxes
5
+ * whose base grows, so a stretched button is filled edge to edge (this outer ::slotted rule wins
6
+ * over the button's own :host display). Both rules skip the same children that never render.
7
+ */
8
+ export const buttonGroupStyles = `${hostTheme}
9
+ :host { display: inline-flex; vertical-align: middle; }
10
+ .base { display: inline-flex; border-radius: var(--_neu-r); box-shadow: var(--_neu-raised); }
11
+ :host([orientation='vertical']) .base { flex-direction: column; }
12
+ ::slotted(*) { --_neu-grp-shadow: 0 0 transparent; --_neu-grp-ss: 0; --_neu-grp-se: 0; --_neu-grp-es: 0; --_neu-grp-ee: 0; }
13
+ ::slotted(:not([hidden], template, script, style)) { display: flex; }
14
+ ::slotted(:focus-visible) { position: relative; z-index: 1; }
15
+ :host(:not([orientation='vertical'])) ::slotted(:nth-child(1 of :not([hidden], template, script, style))) { --_neu-grp-ss: var(--_neu-r); --_neu-grp-es: var(--_neu-r); }
16
+ :host(:not([orientation='vertical'])) ::slotted(:nth-last-child(1 of :not([hidden], template, script, style))) { --_neu-grp-se: var(--_neu-r); --_neu-grp-ee: var(--_neu-r); }
17
+ :host([orientation='vertical']) ::slotted(:nth-child(1 of :not([hidden], template, script, style))) { --_neu-grp-ss: var(--_neu-r); --_neu-grp-se: var(--_neu-r); }
18
+ :host([orientation='vertical']) ::slotted(:nth-last-child(1 of :not([hidden], template, script, style))) { --_neu-grp-es: var(--_neu-r); --_neu-grp-ee: var(--_neu-r); }
19
+ `;
@@ -0,0 +1,14 @@
1
+ import { NeuElement } from '../../core/neu-element.js';
2
+ export declare class NeuDivider extends NeuElement {
3
+ static def: import("../../ssr.js").ComponentDef<{
4
+ readonly orientation: {
5
+ readonly type: "string";
6
+ readonly default: "horizontal";
7
+ readonly values: readonly ["horizontal", "vertical"];
8
+ readonly reflect: true;
9
+ };
10
+ }>;
11
+ orientation: 'horizontal' | 'vertical';
12
+ constructor();
13
+ protected update(changed: ReadonlySet<string>): void;
14
+ }
@@ -0,0 +1,8 @@
1
+ export declare const dividerDef: import("../../core/define.js").ComponentDef<{
2
+ readonly orientation: {
3
+ readonly type: "string";
4
+ readonly default: "horizontal";
5
+ readonly values: readonly ["horizontal", "vertical"];
6
+ readonly reflect: true;
7
+ };
8
+ }>;
@@ -0,0 +1,24 @@
1
+ import { defineComponent } from '../../core/define.js';
2
+ import { html } from '../../core/html.js';
3
+ import { themeCssProperties } from '../../core/styles.js';
4
+ import { dividerStyles } from './divider.styles.js';
5
+ export const dividerDef = defineComponent({
6
+ tag: 'neu-divider',
7
+ props: {
8
+ orientation: {
9
+ type: 'string',
10
+ default: 'horizontal',
11
+ values: ['horizontal', 'vertical'],
12
+ reflect: true,
13
+ },
14
+ },
15
+ styles: dividerStyles,
16
+ template: () => html `<span part="base" class="base"></span>`,
17
+ docs: {
18
+ summary: 'A thin engraved line that separates content.',
19
+ events: [],
20
+ slots: [],
21
+ parts: [{ name: 'base', description: 'The engraved line.' }],
22
+ cssProperties: themeCssProperties,
23
+ },
24
+ });
@@ -0,0 +1,13 @@
1
+ import { NeuElement } from '../../core/neu-element.js';
2
+ import { dividerDef } from './divider.def.js';
3
+ export class NeuDivider extends NeuElement {
4
+ static def = dividerDef;
5
+ constructor() {
6
+ super();
7
+ this.internals.role = 'separator';
8
+ }
9
+ update(changed) {
10
+ if (changed.has('orientation'))
11
+ this.internals.ariaOrientation = this.orientation;
12
+ }
13
+ }
@@ -0,0 +1 @@
1
+ export declare const dividerStyles = "\n:host {\n --_neu-bg: var(--neu-bg, #e0e5ec);\n --_neu-accent-c: var(--neu-accent, #6d5dfc);\n --_neu-d: var(--neu-distance, 6px);\n --_neu-k: var(--neu-intensity, 1);\n --_neu-r: var(--neu-radius, 12px);\n --_neu-danger: var(--neu-danger, #e5484d);\n --_neu-dur: var(--neu-duration, 150ms);\n --_neu-sd: var(--neu-shadow-dark, color-mix(in oklab, var(--_neu-bg), black min(100%, calc(var(--_neu-shade, 18%) * var(--_neu-k)))));\n --_neu-sl: var(--neu-shadow-light, color-mix(in oklab, var(--_neu-bg), white min(100%, calc(var(--_neu-light, 100%) * var(--_neu-k)))));\n --_neu-fg: var(--neu-fg, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-mix, 32%)));\n --_neu-fg-muted: var(--neu-fg-muted, color-mix(in oklab, var(--_neu-bg), var(--_neu-ink, black) var(--_neu-fg-muted-mix, 18%)));\n --_neu-accent-soft: color-mix(in oklab, var(--_neu-accent-c) 25%, var(--_neu-bg));\n --_neu-focus: 0 0 0 3px color-mix(in oklab, var(--_neu-accent-c) 45%, transparent);\n --_neu-raised: var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-raised-sm: calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n --_neu-inset: inset var(--_neu-d) var(--_neu-d) calc(var(--_neu-d) * 2) var(--_neu-sd), inset calc(var(--_neu-d) * -1) calc(var(--_neu-d) * -1) calc(var(--_neu-d) * 2) var(--_neu-sl);\n --_neu-inset-sm: inset calc(var(--_neu-d) * 0.5) calc(var(--_neu-d) * 0.5) var(--_neu-d) var(--_neu-sd), inset calc(var(--_neu-d) * -0.5) calc(var(--_neu-d) * -0.5) var(--_neu-d) var(--_neu-sl);\n font-family: var(--neu-font);\n color: var(--_neu-fg);\n -webkit-tap-highlight-color: transparent;\n}\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n:host(:focus) { outline: none; }\n*, *::before, *::after { box-sizing: border-box; }\n\n:host { display: block; margin-block: 0.75rem; }\n.base { display: block; border-block-start: 1px solid var(--_neu-sd); border-block-end: 1px solid var(--_neu-sl); }\n:host([orientation='vertical']) { display: inline-block; align-self: stretch; min-block-size: 1em; margin-block: 0; margin-inline: 0.75rem; }\n:host([orientation='vertical']) .base { block-size: 100%; min-block-size: 1em; border-block: 0; border-inline-start: 1px solid var(--_neu-sd); border-inline-end: 1px solid var(--_neu-sl); }\n";
@@ -0,0 +1,7 @@
1
+ import { hostTheme } from '../../core/styles.js';
2
+ export const dividerStyles = `${hostTheme}
3
+ :host { display: block; margin-block: 0.75rem; }
4
+ .base { display: block; border-block-start: 1px solid var(--_neu-sd); border-block-end: 1px solid var(--_neu-sl); }
5
+ :host([orientation='vertical']) { display: inline-block; align-self: stretch; min-block-size: 1em; margin-block: 0; margin-inline: 0.75rem; }
6
+ :host([orientation='vertical']) .base { block-size: 100%; min-block-size: 1em; border-block: 0; border-inline-start: 1px solid var(--_neu-sd); border-inline-end: 1px solid var(--_neu-sl); }
7
+ `;
@@ -0,0 +1,12 @@
1
+ export interface IconDrawer {
2
+ draw(): void;
3
+ connected(): void;
4
+ disconnected(): void;
5
+ }
6
+ /**
7
+ * Keeps an element's icon container in step with its library and name: on prop changes and when
8
+ * the library is registered again (while connected, or while the element was away), and never
9
+ * twice for the same icon, so hydration and DOM moves write nothing. Used by neu-icon and
10
+ * neu-icon-button. A closure rather than a class, so the minifier can shorten its state.
11
+ */
12
+ export declare function iconDrawer(box: () => Element | null, library: () => string, name: () => string): IconDrawer;
@@ -0,0 +1,38 @@
1
+ import { renderIconInto } from './icon-loader.js';
2
+ import { iconLibraryVersion, onIconLibraryChange } from './icon-registry.js';
3
+ /**
4
+ * Keeps an element's icon container in step with its library and name: on prop changes and when
5
+ * the library is registered again (while connected, or while the element was away), and never
6
+ * twice for the same icon, so hydration and DOM moves write nothing. Used by neu-icon and
7
+ * neu-icon-button. A closure rather than a class, so the minifier can shorten its state.
8
+ */
9
+ export function iconDrawer(box, library, name) {
10
+ /** Library, name and library version of what the container shows or is loading. */
11
+ let shown;
12
+ let unsubscribe;
13
+ const key = () => `${library()}\n${name()}\n${iconLibraryVersion(library())}`;
14
+ const draw = () => {
15
+ const container = box();
16
+ if (!container)
17
+ return;
18
+ // The first draw finds the template's markup, rendered here or on the server: a system icon
19
+ // is already in place.
20
+ if (shown === undefined && library() === 'system')
21
+ shown = key();
22
+ if (key() === shown)
23
+ return;
24
+ shown = key();
25
+ renderIconInto(container, library(), name());
26
+ };
27
+ return {
28
+ draw,
29
+ connected: () => {
30
+ unsubscribe ??= onIconLibraryChange(draw);
31
+ draw();
32
+ },
33
+ disconnected: () => {
34
+ unsubscribe?.();
35
+ unsubscribe = undefined;
36
+ },
37
+ };
38
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Fills `container` with the icon: the system set at once, other libraries asynchronously.
3
+ * Unknown names, a throwing library and failed loads leave it empty. Never throws.
4
+ */
5
+ export declare function renderIconInto(container: Element, library: string, name: string): void;
@@ -0,0 +1,102 @@
1
+ import { getIconLibrary } from './icon-registry.js';
2
+ import { systemIconSvg } from './system-icons.js';
3
+ const SVG_NS = 'http://www.w3.org/2000/svg';
4
+ /** Static shapes, paint servers and structure only. Case matters: XML names are case-sensitive. */
5
+ const ALLOWED = new Set('svg g path circle ellipse line polyline polygon rect defs title desc symbol linearGradient radialGradient stop clipPath mask use'.split(' '));
6
+ /** Sanitized SVG elements by URL; the promise is shared so one URL is fetched once. */
7
+ const cache = new Map();
8
+ /** The latest request per container; a response for an older one is dropped. */
9
+ const tokens = new WeakMap();
10
+ let parser;
11
+ let counter = 0;
12
+ /**
13
+ * Tested against `name=value` of every attribute: event handlers, style, an href that is not a
14
+ * local #id, and any value that could make the page send a request (url() other than url(#id),
15
+ * image-set(), src(), or a CSS escape, which could spell any of them).
16
+ */
17
+ const UNSAFE = /^on|^style=|href=(?!#)|\\|(?:image-set|src)\(|url\((?![\s'"]*#)/i;
18
+ /** Allowlist walk: keeps allowed SVG elements and text, drops everything else. */
19
+ function clean(parent) {
20
+ for (const { name, value } of [...parent.attributes]) {
21
+ if (UNSAFE.test(`${name}=${value}`))
22
+ parent.removeAttribute(name);
23
+ }
24
+ for (const node of [...parent.childNodes]) {
25
+ if (node.nodeType === 3)
26
+ continue;
27
+ if (node.nodeType !== 1 ||
28
+ node.namespaceURI !== SVG_NS ||
29
+ !ALLOWED.has(node.localName)) {
30
+ node.remove();
31
+ continue;
32
+ }
33
+ clean(node);
34
+ }
35
+ }
36
+ /**
37
+ * Parses SVG text as XML and returns the cleaned root, or null. The result is only ever inserted
38
+ * as nodes, never serialized back into HTML, so no parser differential can reintroduce markup.
39
+ */
40
+ function sanitize(text) {
41
+ parser ??= new DOMParser();
42
+ const source = text.replace(/<svg\b(?![^>]*\sxmlns\s*=)/, `<svg xmlns="${SVG_NS}"`);
43
+ const svg = parser.parseFromString(source, 'image/svg+xml').documentElement;
44
+ if (svg.namespaceURI !== SVG_NS || svg.localName !== 'svg' || svg.querySelector('parsererror')) {
45
+ return null;
46
+ }
47
+ clean(svg);
48
+ svg.removeAttribute('xmlns:xlink');
49
+ svg.setAttribute('part', 'svg');
50
+ svg.setAttribute('aria-hidden', 'true');
51
+ svg.setAttribute('focusable', 'false');
52
+ return svg;
53
+ }
54
+ function load(url) {
55
+ let entry = cache.get(url);
56
+ if (!entry) {
57
+ entry = (async () => {
58
+ try {
59
+ const response = await fetch(url);
60
+ return response.ok ? sanitize((await response.text()).trim()) : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ })();
66
+ cache.set(url, entry);
67
+ // A failure is not remembered, so the next request can try again.
68
+ void entry.then((svg) => svg === null && cache.delete(url));
69
+ }
70
+ return entry;
71
+ }
72
+ /**
73
+ * Fills `container` with the icon: the system set at once, other libraries asynchronously.
74
+ * Unknown names, a throwing library and failed loads leave it empty. Never throws.
75
+ */
76
+ export function renderIconInto(container, library, name) {
77
+ const token = ++counter;
78
+ tokens.set(container, token);
79
+ const show = (svg) => {
80
+ if (tokens.get(container) !== token)
81
+ return;
82
+ container.replaceChildren(...(svg ? [container.ownerDocument.importNode(svg, true)] : []));
83
+ };
84
+ if (library === 'system') {
85
+ // Constant, library-owned markup.
86
+ container.innerHTML = systemIconSvg(name) ?? '';
87
+ return;
88
+ }
89
+ try {
90
+ const source = name ? String(getIconLibrary(library)?.resolve(name) ?? '').trim() : '';
91
+ if (source.startsWith('<'))
92
+ show(sanitize(source));
93
+ // A blank source stays empty: fetch('') would request the current page.
94
+ else if (source)
95
+ void load(source).then(show);
96
+ else
97
+ show(null);
98
+ }
99
+ catch {
100
+ show(null);
101
+ }
102
+ }
@@ -0,0 +1,11 @@
1
+ export interface IconLibrary {
2
+ /** Markup that starts with `<` is an SVG; any other string, and a URL, is fetched. */
3
+ resolve(name: string): string | URL;
4
+ }
5
+ /** Registers a library. Registering a name again replaces it and reloads the icons using it. */
6
+ export declare function registerIconLibrary(name: string, library: IconLibrary): void;
7
+ export declare function getIconLibrary(name: string): IconLibrary | undefined;
8
+ /** Changes every time the library is registered; 0 before the first time. */
9
+ export declare function iconLibraryVersion(name: string): number;
10
+ /** Calls `listener` with the name of every library that gets (re-)registered. Returns the unsubscribe. */
11
+ export declare function onIconLibraryChange(listener: (library: string) => void): () => void;
@@ -0,0 +1,26 @@
1
+ const libraries = new Map();
2
+ /** How often each library has been registered, so a drawn icon knows when it went stale. */
3
+ const versions = new Map();
4
+ const listeners = new Set();
5
+ /** Registers a library. Registering a name again replaces it and reloads the icons using it. */
6
+ export function registerIconLibrary(name, library) {
7
+ // The built-in set is not a registered library and cannot be replaced.
8
+ if (name === 'system')
9
+ return;
10
+ libraries.set(name, library);
11
+ versions.set(name, iconLibraryVersion(name) + 1);
12
+ for (const listener of [...listeners])
13
+ listener(name);
14
+ }
15
+ export function getIconLibrary(name) {
16
+ return libraries.get(name);
17
+ }
18
+ /** Changes every time the library is registered; 0 before the first time. */
19
+ export function iconLibraryVersion(name) {
20
+ return versions.get(name) ?? 0;
21
+ }
22
+ /** Calls `listener` with the name of every library that gets (re-)registered. Returns the unsubscribe. */
23
+ export function onIconLibraryChange(listener) {
24
+ listeners.add(listener);
25
+ return () => listeners.delete(listener);
26
+ }
@@ -0,0 +1,25 @@
1
+ import { NeuElement } from '../../core/neu-element.js';
2
+ export declare class NeuIcon extends NeuElement {
3
+ static def: import("../../ssr.js").ComponentDef<{
4
+ readonly name: {
5
+ readonly type: "string";
6
+ readonly reflect: true;
7
+ };
8
+ readonly library: {
9
+ readonly type: "string";
10
+ readonly default: "system";
11
+ readonly reflect: true;
12
+ };
13
+ readonly label: {
14
+ readonly type: "string";
15
+ readonly reflect: true;
16
+ };
17
+ }>;
18
+ name: string;
19
+ library: string;
20
+ label: string;
21
+ private readonly drawer;
22
+ protected update(changed: ReadonlySet<string>): void;
23
+ protected connected(): void;
24
+ protected disconnected(): void;
25
+ }
@@ -0,0 +1,15 @@
1
+ export declare const iconDef: import("../../core/define.js").ComponentDef<{
2
+ readonly name: {
3
+ readonly type: "string";
4
+ readonly reflect: true;
5
+ };
6
+ readonly library: {
7
+ readonly type: "string";
8
+ readonly default: "system";
9
+ readonly reflect: true;
10
+ };
11
+ readonly label: {
12
+ readonly type: "string";
13
+ readonly reflect: true;
14
+ };
15
+ }>;
@@ -0,0 +1,24 @@
1
+ import { defineComponent } from '../../core/define.js';
2
+ import { html, unsafeHTML } from '../../core/html.js';
3
+ import { iconStyles } from './icon.styles.js';
4
+ import { systemIconSvg } from './system-icons.js';
5
+ export const iconDef = defineComponent({
6
+ tag: 'neu-icon',
7
+ props: {
8
+ name: { type: 'string', reflect: true },
9
+ library: { type: 'string', default: 'system', reflect: true },
10
+ label: { type: 'string', reflect: true },
11
+ },
12
+ styles: iconStyles,
13
+ template: (p) => {
14
+ const svg = p.library === 'system' ? systemIconSvg(p.name) : undefined;
15
+ return html `<span class="box" data-ref="box">${svg === undefined ? '' : unsafeHTML(svg)}</span>`;
16
+ },
17
+ docs: {
18
+ summary: 'A vector icon from the built-in set or a registered icon library.',
19
+ events: [],
20
+ slots: [],
21
+ parts: [{ name: 'svg', description: 'The rendered SVG element.' }],
22
+ cssProperties: [],
23
+ },
24
+ });
@@ -0,0 +1,23 @@
1
+ import { NeuElement } from '../../core/neu-element.js';
2
+ import { iconDrawer } from './icon-drawer.js';
3
+ import { iconDef } from './icon.def.js';
4
+ export class NeuIcon extends NeuElement {
5
+ static def = iconDef;
6
+ drawer = iconDrawer(() => this.ref('box'), () => String(this.props.library), () => String(this.props.name));
7
+ update(changed) {
8
+ if (changed.has('label')) {
9
+ const label = String(this.props.label);
10
+ this.internals.role = label ? 'img' : null;
11
+ this.internals.ariaLabel = label || null;
12
+ this.internals.ariaHidden = label ? null : 'true';
13
+ }
14
+ if (changed.has('name') || changed.has('library'))
15
+ this.drawer.draw();
16
+ }
17
+ connected() {
18
+ this.drawer.connected();
19
+ }
20
+ disconnected() {
21
+ this.drawer.disconnected();
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ export declare const iconStyles = "\n:host { display: inline-block; inline-size: 1em; block-size: 1em; vertical-align: -0.125em; color: inherit; }\n:host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }\n:host([hidden]) { display: none !important; }\n.box, svg { display: block; inline-size: 100%; block-size: 100%; }\n.box { contain: paint; }\n";
@@ -0,0 +1,7 @@
1
+ export const iconStyles = `
2
+ :host { display: inline-block; inline-size: 1em; block-size: 1em; vertical-align: -0.125em; color: inherit; }
3
+ :host(:not(:defined)) { visibility: inherit !important; min-block-size: auto !important; }
4
+ :host([hidden]) { display: none !important; }
5
+ .box, svg { display: block; inline-size: 100%; block-size: 100%; }
6
+ .box { contain: paint; }
7
+ `;
@@ -0,0 +1,2 @@
1
+ /** The built-in icon as an SVG string, or undefined for an unknown name. DOM-free. */
2
+ export declare function systemIconSvg(name: string): string | undefined;
@@ -0,0 +1,17 @@
1
+ const BODIES = {
2
+ check: '<path d="M5 12.5l4.5 4.5L19 7.5"/>',
3
+ 'chevron-down': '<path d="M6 9l6 6 6-6"/>',
4
+ 'chevron-up': '<path d="M6 15l6-6 6 6"/>',
5
+ plus: '<path d="M12 5v14M5 12h14"/>',
6
+ minus: '<path d="M5 12h14"/>',
7
+ x: '<path d="M6 6l12 12M18 6L6 18"/>',
8
+ 'alert-circle': '<circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.5h.01"/>',
9
+ user: '<circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-6 8-6s8 2 8 6"/>',
10
+ };
11
+ /** The built-in icon as an SVG string, or undefined for an unknown name. DOM-free. */
12
+ export function systemIconSvg(name) {
13
+ const body = Object.hasOwn(BODIES, name) ? BODIES[name] : undefined;
14
+ if (body === undefined)
15
+ return undefined;
16
+ return `<svg part="svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${body}</svg>`;
17
+ }
@@ -0,0 +1,51 @@
1
+ import { NeuButton } from '../button/button.js';
2
+ export declare class NeuIconButton extends NeuButton {
3
+ static def: import("../../ssr.js").ComponentDef<{
4
+ readonly icon: {
5
+ readonly type: "string";
6
+ };
7
+ readonly library: {
8
+ readonly type: "string";
9
+ readonly default: "system";
10
+ };
11
+ readonly label: {
12
+ readonly type: "string";
13
+ };
14
+ readonly shape: {
15
+ readonly type: "string";
16
+ readonly default: "circle";
17
+ readonly values: readonly ["circle", "square"];
18
+ readonly reflect: true;
19
+ };
20
+ readonly variant: {
21
+ readonly type: "string";
22
+ readonly default: "default";
23
+ readonly values: readonly ["default", "primary", "flat"];
24
+ readonly reflect: true;
25
+ };
26
+ readonly size: {
27
+ readonly type: "string";
28
+ readonly default: "md";
29
+ readonly values: readonly ["sm", "md", "lg"];
30
+ readonly reflect: true;
31
+ };
32
+ readonly disabled: {
33
+ readonly type: "boolean";
34
+ readonly reflect: true;
35
+ };
36
+ readonly type: {
37
+ readonly type: "string";
38
+ readonly default: "button";
39
+ readonly values: readonly ["button", "submit", "reset"];
40
+ readonly reflect: true;
41
+ };
42
+ }>;
43
+ icon: string;
44
+ library: string;
45
+ label: string;
46
+ shape: 'circle' | 'square';
47
+ private readonly drawer;
48
+ protected update(changed: ReadonlySet<string>): void;
49
+ protected connected(): void;
50
+ protected disconnected(): void;
51
+ }
@@ -0,0 +1,40 @@
1
+ export declare const iconButtonDef: import("../../core/define.js").ComponentDef<{
2
+ readonly icon: {
3
+ readonly type: "string";
4
+ };
5
+ readonly library: {
6
+ readonly type: "string";
7
+ readonly default: "system";
8
+ };
9
+ readonly label: {
10
+ readonly type: "string";
11
+ };
12
+ readonly shape: {
13
+ readonly type: "string";
14
+ readonly default: "circle";
15
+ readonly values: readonly ["circle", "square"];
16
+ readonly reflect: true;
17
+ };
18
+ readonly variant: {
19
+ readonly type: "string";
20
+ readonly default: "default";
21
+ readonly values: readonly ["default", "primary", "flat"];
22
+ readonly reflect: true;
23
+ };
24
+ readonly size: {
25
+ readonly type: "string";
26
+ readonly default: "md";
27
+ readonly values: readonly ["sm", "md", "lg"];
28
+ readonly reflect: true;
29
+ };
30
+ readonly disabled: {
31
+ readonly type: "boolean";
32
+ readonly reflect: true;
33
+ };
34
+ readonly type: {
35
+ readonly type: "string";
36
+ readonly default: "button";
37
+ readonly values: readonly ["button", "submit", "reset"];
38
+ readonly reflect: true;
39
+ };
40
+ }>;
@@ -0,0 +1,44 @@
1
+ import { defineComponent } from '../../core/define.js';
2
+ import { html, unsafeHTML } from '../../core/html.js';
3
+ import { themeCssProperties } from '../../core/styles.js';
4
+ import { systemIconSvg } from '../icon/system-icons.js';
5
+ import { iconButtonStyles } from './icon-button.styles.js';
6
+ export const iconButtonDef = defineComponent({
7
+ tag: 'neu-icon-button',
8
+ props: {
9
+ icon: { type: 'string' },
10
+ library: { type: 'string', default: 'system' },
11
+ label: { type: 'string' },
12
+ shape: { type: 'string', default: 'circle', values: ['circle', 'square'], reflect: true },
13
+ variant: {
14
+ type: 'string',
15
+ default: 'default',
16
+ values: ['default', 'primary', 'flat'],
17
+ reflect: true,
18
+ },
19
+ size: { type: 'string', default: 'md', values: ['sm', 'md', 'lg'], reflect: true },
20
+ disabled: { type: 'boolean', reflect: true },
21
+ type: {
22
+ type: 'string',
23
+ default: 'button',
24
+ values: ['button', 'submit', 'reset'],
25
+ reflect: true,
26
+ },
27
+ },
28
+ styles: iconButtonStyles,
29
+ template: (p) => {
30
+ const svg = p.library === 'system' ? systemIconSvg(p.icon) : undefined;
31
+ return html `<span part="base" class="base"><span part="icon" class="icon" data-ref="icon">${svg === undefined ? '' : unsafeHTML(svg)}</span></span>`;
32
+ },
33
+ docs: {
34
+ summary: 'A raised button that shows an icon instead of a label. Name it with label.',
35
+ events: [{ name: 'click', description: 'Fired on activation; never while disabled.' }],
36
+ slots: [],
37
+ parts: [
38
+ { name: 'base', description: 'The raised surface.' },
39
+ { name: 'icon', description: 'Wrapper around the icon.' },
40
+ { name: 'svg', description: 'The rendered SVG element.' },
41
+ ],
42
+ cssProperties: themeCssProperties,
43
+ },
44
+ });
@@ -0,0 +1,21 @@
1
+ import { NeuButton } from '../button/button.js';
2
+ import { iconDrawer } from '../icon/icon-drawer.js';
3
+ import { iconButtonDef } from './icon-button.def.js';
4
+ export class NeuIconButton extends NeuButton {
5
+ static def = iconButtonDef;
6
+ drawer = iconDrawer(() => this.ref('icon'), () => String(this.props.library), () => String(this.props.icon));
7
+ update(changed) {
8
+ super.update(changed);
9
+ if (changed.has('label') || changed.has('icon')) {
10
+ this.internals.ariaLabel = String(this.props.label || this.props.icon) || null;
11
+ }
12
+ if (changed.has('icon') || changed.has('library'))
13
+ this.drawer.draw();
14
+ }
15
+ connected() {
16
+ this.drawer.connected();
17
+ }
18
+ disconnected() {
19
+ this.drawer.disconnected();
20
+ }
21
+ }
@@ -0,0 +1 @@
1
+ export declare const iconButtonStyles: string;
@@ -0,0 +1,12 @@
1
+ import { buttonBaseStyles } from '../button/button.styles.js';
2
+ export const iconButtonStyles = `${buttonBaseStyles}
3
+ :host { inline-size: 2.5rem; block-size: 2.5rem; }
4
+ :host([size='sm']) { inline-size: 2rem; block-size: 2rem; }
5
+ :host([size='lg']) { inline-size: 3rem; block-size: 3rem; }
6
+ .base { inline-size: 100%; block-size: 100%; --_neu-rr: 50%; }
7
+ :host([shape='square']) .base { --_neu-rr: var(--_neu-r); }
8
+ .icon { display: block; inline-size: 1.125rem; block-size: 1.125rem; contain: paint; }
9
+ :host([size='sm']) .icon { inline-size: 1rem; block-size: 1rem; }
10
+ :host([size='lg']) .icon { inline-size: 1.375rem; block-size: 1.375rem; }
11
+ .icon svg { display: block; inline-size: 100%; block-size: 100%; }
12
+ `;