@ds-mo/ui 14.11.1 → 14.13.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.
- package/NOTICE +10 -0
- package/dist/.package-ready.json +1 -1
- package/dist/agent-patterns.json +1 -1
- package/dist/agent.json +104 -9
- package/dist/angular/ds-pagination.d.ts +1 -1
- package/dist/angular/ds-pagination.js +3 -3
- package/dist/angular/ds-paper-texture.d.ts +12 -0
- package/dist/angular/ds-paper-texture.js +38 -0
- package/dist/angular/ds-shell-app.d.ts +1 -1
- package/dist/angular/ds-shell-app.js +3 -3
- package/dist/angular/ds-table.d.ts +1 -1
- package/dist/angular/ds-table.js +3 -3
- package/dist/angular/index.d.ts +1 -1
- package/dist/angular/index.js +1 -0
- package/dist/angular/proxies.d.ts +1 -0
- package/dist/angular/proxies.js +1 -0
- package/dist/components/ds-agent-questionnaire.js +1 -1
- package/dist/components/ds-agent-response.js +1 -1
- package/dist/components/ds-badge.js +1 -1
- package/dist/components/ds-banner.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-title.js +1 -1
- package/dist/components/ds-bar-workflow.js +1 -1
- package/dist/components/ds-bar-workflow.js.map +1 -1
- package/dist/components/ds-button-filled.js +1 -1
- package/dist/components/ds-button-unfilled.js +1 -1
- package/dist/components/ds-card-chart.js +1 -1
- package/dist/components/ds-card-setting.js +1 -1
- package/dist/components/ds-code-block.js +1 -1
- package/dist/components/ds-conversation-list-item.js +1 -1
- package/dist/components/ds-conversation-list-item.js.map +1 -1
- package/dist/components/ds-filter-menu.js +1 -1
- package/dist/components/ds-filter-menu.js.map +1 -1
- package/dist/components/ds-input.js +1 -1
- package/dist/components/ds-markdown.js +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-message-actions.js +1 -1
- package/dist/components/ds-message-composer.js +1 -1
- package/dist/components/ds-message-scroller.js +1 -1
- package/dist/components/ds-mobile-bar-nav.js +1 -1
- package/dist/components/ds-mobile-header.js +1 -1
- package/dist/components/ds-mobile-section-switcher.js +1 -1
- package/dist/components/ds-mobile-sheet-nav.js +1 -1
- package/dist/components/ds-modal.js +1 -1
- package/dist/components/ds-pagination.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-tool-header.js +1 -1
- package/dist/components/ds-panel-tool-search.js +1 -1
- package/dist/components/ds-panel-tools.js +1 -1
- package/dist/components/ds-paper-texture.d.ts +11 -0
- package/dist/components/ds-paper-texture.js +2 -0
- package/dist/components/ds-paper-texture.js.map +1 -0
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-shell-app.js +1 -1
- package/dist/components/ds-shell-app.js.map +1 -1
- package/dist/components/ds-shell-page.js +1 -1
- package/dist/components/ds-shell-tools.js +1 -1
- package/dist/components/ds-swatch-picker.js +1 -1
- package/dist/components/ds-tab-group.js +1 -1
- package/dist/components/ds-table-saved-views.js +1 -1
- package/dist/components/ds-table-saved-views.js.map +1 -1
- package/dist/components/ds-table-toolbar.js +1 -1
- package/dist/components/ds-table.js +1 -1
- package/dist/components/ds-table.js.map +1 -1
- package/dist/components/ds-toast.js +1 -1
- package/dist/components/ds-typing-indicator.js +1 -1
- package/dist/components/p-BEvBJ65v.js +2 -0
- package/dist/components/p-BEvBJ65v.js.map +1 -0
- package/dist/components/{p-DvJkkdIO.js → p-BxqBiVbH.js} +2 -2
- package/dist/components/p-BzLnSwG9.js +2 -0
- package/dist/components/p-BzLnSwG9.js.map +1 -0
- package/dist/components/{p-CbfmzgdW.js → p-BzObtSlo.js} +2 -2
- package/dist/components/{p-Bv1CFHgD.js → p-C2n8A8NA.js} +2 -2
- package/dist/components/p-C5eosu67.js +2 -0
- package/dist/components/p-C5eosu67.js.map +1 -0
- package/dist/components/{p-c_5SXjwx.js → p-C7rzVotG.js} +2 -2
- package/dist/components/{p-bQm3gfr1.js → p-CKJzHclc.js} +2 -2
- package/dist/components/p-CWDmHPJC.js +2 -0
- package/dist/components/p-CWDmHPJC.js.map +1 -0
- package/dist/components/{p-BPnmiiIS.js → p-CjgnFGd6.js} +2 -2
- package/dist/components/{p-BuGGwWmt.js → p-Ck_VRQYi.js} +2 -2
- package/dist/components/p-CnWC0QvG.js +2 -0
- package/dist/components/p-CnWC0QvG.js.map +1 -0
- package/dist/components/p-CooNucm0.js +2 -0
- package/dist/components/p-CooNucm0.js.map +1 -0
- package/dist/components/{p-CoQSB3tl.js → p-Ct-m0I6U.js} +2 -2
- package/dist/components/{p-CqbMoVDQ.js → p-Cyw9odJX.js} +2 -2
- package/dist/components/{p-D5VglrfF.js → p-D1Nqlrrl.js} +2 -2
- package/dist/components/p-DIEx1e0M.js +2 -0
- package/dist/components/p-DIEx1e0M.js.map +1 -0
- package/dist/components/{p-DjqZWBnM.js → p-DL_5VENG.js} +2 -2
- package/dist/components/p-DfyfrfHL.js +2 -0
- package/dist/components/p-DfyfrfHL.js.map +1 -0
- package/dist/components/{p-CvwtYbmh.js → p-Dsn1RCS5.js} +2 -2
- package/dist/components/{p-D5bPuNiF.js → p-M9DPfXbP.js} +2 -2
- package/dist/components/{p-DgWGfscR.js → p-Rfpd5mWO.js} +2 -2
- package/dist/components/p-URdgCwEs.js +2 -0
- package/dist/components/{p-BCg_6ErW.js.map → p-URdgCwEs.js.map} +1 -1
- package/dist/components/{p-1tOy4H51.js → p-V2ywPfsR.js} +2 -2
- package/dist/components/{p-BVmKcpyP.js → p-iBuPFJL9.js} +2 -2
- package/dist/components/p-vWK5faWZ.js +2 -0
- package/dist/components/p-vWK5faWZ.js.map +1 -0
- package/dist/docs/components.json +163 -5
- package/dist/lib/shell/index.js.map +2 -2
- package/dist/lib/shell/shell-responsive.d.ts +1 -1
- package/dist/mcp-data/registry/bar-nav.json +1 -1
- package/dist/mcp-data/registry/filter-menu.json +1 -1
- package/dist/mcp-data/registry/menu.json +1 -1
- package/dist/mcp-data/registry/pagination.json +21 -3
- package/dist/mcp-data/registry/panel-nav.json +1 -1
- package/dist/mcp-data/registry/panel-tools.json +1 -1
- package/dist/mcp-data/registry/paper-texture.json +141 -0
- package/dist/mcp-data/registry/registry.json +106 -8
- package/dist/mcp-data/registry/select.json +2 -2
- package/dist/mcp-data/registry/shell-app.json +26 -5
- package/dist/mcp-data/registry/shell-tools.json +2 -2
- package/dist/mcp-data/registry/table-saved-views.json +6 -6
- package/dist/mcp-data/registry/table.json +31 -13
- package/dist/react/components.d.ts +1 -0
- package/dist/react/components.js +1 -0
- package/dist/react/ds-paper-texture.d.ts +9 -0
- package/dist/react/ds-paper-texture.js +12 -0
- package/dist/styles/table.css +5 -0
- package/dist/types/components/Pagination/Pagination.d.ts +2 -0
- package/dist/types/components/Pagination/pagination-types.d.ts +2 -0
- package/dist/types/components/PanelTools/PanelTools.d.ts +1 -0
- package/dist/types/components/PaperTexture/PaperTexture.d.ts +18 -0
- package/dist/types/components/PaperTexture/paper-texture-types.d.ts +39 -0
- package/dist/types/components/ShellApp/ShellApp.d.ts +7 -2
- package/dist/types/components/Table/Table.d.ts +19 -1
- package/dist/types/components/Table/table-layout-controller.d.ts +7 -1
- package/dist/types/components/Table/table-model.d.ts +3 -2
- package/dist/types/components/Table/table-viewport-fit-controller.d.ts +5 -1
- package/dist/types/components/Table/table-virtual-controller.d.ts +14 -1
- package/dist/types/components/Table/table-virtual-model.d.ts +26 -1
- package/dist/types/components/TableSavedViews/TableSavedViews.d.ts +1 -1
- package/dist/types/components.d.ts +59 -4
- package/dist/types/shell/shell-responsive.d.ts +1 -1
- package/package.json +4 -2
- package/dist/components/p-BCg_6ErW.js +0 -2
- package/dist/components/p-BFSnIEsC.js +0 -2
- package/dist/components/p-BFSnIEsC.js.map +0 -1
- package/dist/components/p-BiTwXttN.js +0 -2
- package/dist/components/p-BiTwXttN.js.map +0 -1
- package/dist/components/p-BmWWU20X.js +0 -2
- package/dist/components/p-BmWWU20X.js.map +0 -1
- package/dist/components/p-C-L4InQ0.js +0 -2
- package/dist/components/p-C-L4InQ0.js.map +0 -1
- package/dist/components/p-C-Zgq_Fx.js +0 -2
- package/dist/components/p-C-Zgq_Fx.js.map +0 -1
- package/dist/components/p-C1jsgfeh.js +0 -2
- package/dist/components/p-C1jsgfeh.js.map +0 -1
- package/dist/components/p-D8nnhQNJ.js +0 -2
- package/dist/components/p-D8nnhQNJ.js.map +0 -1
- package/dist/components/p-DNpE0RBz.js +0 -2
- package/dist/components/p-DNpE0RBz.js.map +0 -1
- /package/dist/components/{p-DvJkkdIO.js.map → p-BxqBiVbH.js.map} +0 -0
- /package/dist/components/{p-CbfmzgdW.js.map → p-BzObtSlo.js.map} +0 -0
- /package/dist/components/{p-Bv1CFHgD.js.map → p-C2n8A8NA.js.map} +0 -0
- /package/dist/components/{p-c_5SXjwx.js.map → p-C7rzVotG.js.map} +0 -0
- /package/dist/components/{p-bQm3gfr1.js.map → p-CKJzHclc.js.map} +0 -0
- /package/dist/components/{p-BPnmiiIS.js.map → p-CjgnFGd6.js.map} +0 -0
- /package/dist/components/{p-BuGGwWmt.js.map → p-Ck_VRQYi.js.map} +0 -0
- /package/dist/components/{p-CoQSB3tl.js.map → p-Ct-m0I6U.js.map} +0 -0
- /package/dist/components/{p-CqbMoVDQ.js.map → p-Cyw9odJX.js.map} +0 -0
- /package/dist/components/{p-D5VglrfF.js.map → p-D1Nqlrrl.js.map} +0 -0
- /package/dist/components/{p-DjqZWBnM.js.map → p-DL_5VENG.js.map} +0 -0
- /package/dist/components/{p-CvwtYbmh.js.map → p-Dsn1RCS5.js.map} +0 -0
- /package/dist/components/{p-D5bPuNiF.js.map → p-M9DPfXbP.js.map} +0 -0
- /package/dist/components/{p-DgWGfscR.js.map → p-Rfpd5mWO.js.map} +0 -0
- /package/dist/components/{p-1tOy4H51.js.map → p-V2ywPfsR.js.map} +0 -0
- /package/dist/components/{p-BVmKcpyP.js.map → p-iBuPFJL9.js.map} +0 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "paper-texture",
|
|
4
|
+
"title": "PaperTexture",
|
|
5
|
+
"description": "A decorative Paper Design WebGL2 paper texture surface with Storybook-tunable literal shader parameters and a static default frame.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/paper-texture",
|
|
11
|
+
"source": "src/wc/components/PaperTexture/PaperTexture.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "specialized",
|
|
15
|
+
"status": "experimental",
|
|
16
|
+
"summary": "A decorative Paper Design WebGL2 paper texture surface with Storybook-tunable literal shader parameters and a static default frame.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"A shell or other visual surface needs a paper or cardboard texture layered above an existing background.",
|
|
19
|
+
"Designers need to tune paper texture parameters interactively before choosing a baked static asset."
|
|
20
|
+
],
|
|
21
|
+
"avoidWhen": [
|
|
22
|
+
"The surface needs semantic content, interaction, or accessible information.",
|
|
23
|
+
"The target environment cannot provide WebGL2 and no pre-baked fallback asset is available.",
|
|
24
|
+
"A token-driven color treatment is required; this component accepts literal shader colors only."
|
|
25
|
+
],
|
|
26
|
+
"alternatives": [],
|
|
27
|
+
"commonlyComposedWith": [
|
|
28
|
+
"component:ds-shell-app"
|
|
29
|
+
],
|
|
30
|
+
"accessibility": [
|
|
31
|
+
"The texture is decorative and is hidden from assistive technology.",
|
|
32
|
+
"Keep all meaningful content in the owning surface above or beside the texture."
|
|
33
|
+
],
|
|
34
|
+
"states": [
|
|
35
|
+
"The default speed is zero, so a texture renders one deterministic frame without a recurring animation loop.",
|
|
36
|
+
"Paper shader colors are literal CSS color strings and are not resolved from TokoMo tokens.",
|
|
37
|
+
"If WebGL2 or the noise texture is unavailable, the decorative layer removes itself without blocking the owning surface."
|
|
38
|
+
],
|
|
39
|
+
"responsiveBehavior": [
|
|
40
|
+
"The shader observes its host dimensions and follows the owning surface through resize changes.",
|
|
41
|
+
"minPixelRatio and maxPixelCount are available for explicit quality/performance tuning."
|
|
42
|
+
],
|
|
43
|
+
"frameworkCaveats": {
|
|
44
|
+
"customElements": [
|
|
45
|
+
"Assign config as a JavaScript property; nested shader values are not intended for serialized HTML attributes."
|
|
46
|
+
],
|
|
47
|
+
"react": [
|
|
48
|
+
"Use the generated wrapper and pass the config object as a property."
|
|
49
|
+
],
|
|
50
|
+
"angular": [
|
|
51
|
+
"Bind the config object as a property so nested shader values are preserved."
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"references": [
|
|
55
|
+
{
|
|
56
|
+
"label": "Storybook parameter playground",
|
|
57
|
+
"path": "src/wc/components/PaperTexture/PaperTexture.stories.ts"
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"api": {
|
|
62
|
+
"props": {
|
|
63
|
+
"config": {
|
|
64
|
+
"type": "PaperTextureConfig",
|
|
65
|
+
"resolvedType": "PaperTextureConfig",
|
|
66
|
+
"default": "{}",
|
|
67
|
+
"required": false,
|
|
68
|
+
"mutable": false,
|
|
69
|
+
"description": "Paper Design shader parameters. Set as a JavaScript property."
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"events": [],
|
|
73
|
+
"methods": [],
|
|
74
|
+
"slots": []
|
|
75
|
+
},
|
|
76
|
+
"props": {
|
|
77
|
+
"config": {
|
|
78
|
+
"type": "PaperTextureConfig",
|
|
79
|
+
"resolvedType": "PaperTextureConfig",
|
|
80
|
+
"default": "{}",
|
|
81
|
+
"required": false,
|
|
82
|
+
"mutable": false,
|
|
83
|
+
"description": "Paper Design shader parameters. Set as a JavaScript property."
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"events": [],
|
|
87
|
+
"methods": [],
|
|
88
|
+
"slots": [],
|
|
89
|
+
"exports": {
|
|
90
|
+
"customElement": "ds-paper-texture",
|
|
91
|
+
"react": "DsPaperTexture",
|
|
92
|
+
"angular": "DsPaperTexture"
|
|
93
|
+
},
|
|
94
|
+
"consumption": {
|
|
95
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
96
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
97
|
+
"customElements": {
|
|
98
|
+
"import": "import '@ds-mo/ui/dist/components/ds-paper-texture.js';",
|
|
99
|
+
"example": "<ds-paper-texture></ds-paper-texture>"
|
|
100
|
+
},
|
|
101
|
+
"react": {
|
|
102
|
+
"import": "import { DsPaperTexture } from '@ds-mo/ui/react';",
|
|
103
|
+
"example": "<DsPaperTexture />"
|
|
104
|
+
},
|
|
105
|
+
"angular": {
|
|
106
|
+
"import": "import { DsPaperTexture } from '@ds-mo/ui/angular/ds-paper-texture';",
|
|
107
|
+
"example": "<ds-paper-texture></ds-paper-texture>"
|
|
108
|
+
},
|
|
109
|
+
"complexPropertyNote": "Assign these non-primitive values as JavaScript properties: config.",
|
|
110
|
+
"peerDependencies": {
|
|
111
|
+
"required": [
|
|
112
|
+
"@ds-mo/tokens ^6.5.0",
|
|
113
|
+
"@ds-mo/icons ^7.0.0"
|
|
114
|
+
],
|
|
115
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
"dependencies": [
|
|
120
|
+
"@ds-mo/ui",
|
|
121
|
+
"@ds-mo/tokens"
|
|
122
|
+
],
|
|
123
|
+
"registryDependencies": [],
|
|
124
|
+
"files": [
|
|
125
|
+
{
|
|
126
|
+
"path": "src/wc/components/PaperTexture/PaperTexture.css",
|
|
127
|
+
"content": ":host {\n position: absolute;\n inset: 0;\n display: block;\n overflow: hidden;\n pointer-events: none;\n opacity: var(--ds-paper-texture-opacity, 1);\n}\n\n.paper-texture__mount {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n overflow: hidden;\n isolation: isolate;\n}\n\n.paper-texture__mount[data-paper-shader] canvas {\n z-index: 0;\n}\n\n:host([data-paper-texture-error]) {\n display: none;\n}\n",
|
|
128
|
+
"type": "registry:ui"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"path": "src/wc/components/PaperTexture/PaperTexture.tsx",
|
|
132
|
+
"content": "import { Component, Element, Host, Prop, Watch, h } from '@stencil/core';\nimport {\n emptyPixel,\n getShaderColorFromString,\n getShaderNoiseTexture,\n paperTextureFragmentShader,\n ShaderFitOptions,\n ShaderMount,\n type ShaderMountUniforms,\n} from '@paper-design/shaders';\nimport { DEFAULT_PAPER_TEXTURE_CONFIG, type PaperTextureConfig } from './paper-texture-types';\n\nlet emptyPixelPromise: Promise<HTMLImageElement> | undefined;\nlet noiseTexturePromise: Promise<HTMLImageElement> | undefined;\n\nfunction waitForImage(image: HTMLImageElement): Promise<HTMLImageElement> {\n if (image.complete && image.naturalWidth > 0) return Promise.resolve(image);\n\n return new Promise((resolve, reject) => {\n const onLoad = () => {\n cleanup();\n resolve(image);\n };\n const onError = () => {\n cleanup();\n reject(new Error(`Paper texture image failed to load: ${image.src}`));\n };\n const cleanup = () => {\n image.removeEventListener('load', onLoad);\n image.removeEventListener('error', onError);\n };\n\n image.addEventListener('load', onLoad, { once: true });\n image.addEventListener('error', onError, { once: true });\n });\n}\n\nfunction loadImage(source: HTMLImageElement | string | undefined): Promise<HTMLImageElement> {\n if (source instanceof HTMLImageElement) return waitForImage(source);\n\n const image = new Image();\n if (source && source !== emptyPixel) image.crossOrigin = 'anonymous';\n image.src = source || emptyPixel;\n return waitForImage(image);\n}\n\nfunction loadEmptyPixel(): Promise<HTMLImageElement> {\n emptyPixelPromise ??= loadImage(emptyPixel);\n return emptyPixelPromise;\n}\n\nfunction loadNoiseTexture(): Promise<HTMLImageElement> {\n noiseTexturePromise ??= Promise.resolve(getShaderNoiseTexture()).then(image => {\n if (!image) throw new Error('Paper texture noise texture is unavailable');\n return waitForImage(image);\n });\n return noiseTexturePromise;\n}\n\n@Component({\n tag: 'ds-paper-texture',\n styleUrl: 'PaperTexture.css',\n scoped: true,\n})\nexport class PaperTexture {\n @Element() el!: HTMLElement;\n\n /** Paper Design shader parameters. Set as a JavaScript property. */\n @Prop() config: PaperTextureConfig = {};\n\n private shaderMount?: ShaderMount;\n private syncGeneration = 0;\n private hasLoaded = false;\n\n componentDidLoad() {\n this.hasLoaded = true;\n void this.syncShader();\n }\n\n disconnectedCallback() {\n this.syncGeneration += 1;\n this.disposeShader();\n }\n\n @Watch('config')\n onConfigChange() {\n if (this.hasLoaded) void this.syncShader();\n }\n\n private resolvedConfig(): Required<Omit<PaperTextureConfig, 'image'>> &\n Pick<PaperTextureConfig, 'image'> {\n return {\n ...DEFAULT_PAPER_TEXTURE_CONFIG,\n ...(this.config ?? {}),\n };\n }\n\n private async syncShader() {\n const generation = ++this.syncGeneration;\n const config = this.resolvedConfig();\n\n try {\n const [image, noiseTexture] = await Promise.all([\n loadImage(config.image).catch(() => loadEmptyPixel()),\n loadNoiseTexture(),\n ]);\n\n if (generation !== this.syncGeneration || !this.el.isConnected) return;\n\n const uniforms = this.buildUniforms(config, image, noiseTexture);\n const mountElement = this.el.querySelector<HTMLElement>('.paper-texture__mount');\n if (!mountElement) return;\n\n if (!this.shaderMount) {\n this.shaderMount = new ShaderMount(\n mountElement,\n paperTextureFragmentShader,\n uniforms,\n undefined,\n config.speed,\n config.frame,\n config.minPixelRatio,\n config.maxPixelCount,\n ['u_image']\n );\n } else {\n this.shaderMount.setUniforms(uniforms);\n this.shaderMount.setSpeed(config.speed);\n this.shaderMount.setFrame(config.frame);\n this.shaderMount.setMinPixelRatio(config.minPixelRatio);\n this.shaderMount.setMaxPixelCount(config.maxPixelCount);\n }\n\n this.el.removeAttribute('data-paper-texture-error');\n } catch (error) {\n if (generation !== this.syncGeneration) return;\n this.disposeShader();\n this.el.querySelector<HTMLElement>('.paper-texture__mount')?.replaceChildren();\n this.el.setAttribute('data-paper-texture-error', 'unavailable');\n console.warn('[ds-paper-texture] WebGL2 paper texture unavailable.', error);\n }\n }\n\n private buildUniforms(\n config: Required<Omit<PaperTextureConfig, 'image'>> & Pick<PaperTextureConfig, 'image'>,\n image: HTMLImageElement,\n noiseTexture: HTMLImageElement\n ): ShaderMountUniforms {\n return {\n u_image: image,\n u_noiseTexture: noiseTexture,\n u_colorFront: getShaderColorFromString(config.colorFront),\n u_colorBack: getShaderColorFromString(config.colorBack),\n u_contrast: config.contrast,\n u_roughness: config.roughness,\n u_fiber: config.fiber,\n u_fiberSize: config.fiberSize,\n u_crumples: config.crumples,\n u_crumpleSize: config.crumpleSize,\n u_foldCount: config.foldCount,\n u_folds: config.folds,\n u_fade: config.fade,\n u_drops: config.drops,\n u_seed: config.seed,\n u_fit: ShaderFitOptions[config.fit],\n u_scale: config.scale,\n u_rotation: config.rotation,\n u_offsetX: config.offsetX,\n u_offsetY: config.offsetY,\n u_originX: config.originX,\n u_originY: config.originY,\n u_worldWidth: config.worldWidth,\n u_worldHeight: config.worldHeight,\n };\n }\n\n private disposeShader() {\n this.shaderMount?.dispose();\n this.shaderMount = undefined;\n }\n\n render() {\n const config = this.resolvedConfig();\n return (\n <Host aria-hidden=\"true\" style={{ '--ds-paper-texture-opacity': String(config.opacity) }}>\n <div class=\"paper-texture__mount\" />\n </Host>\n );\n }\n}\n",
|
|
133
|
+
"type": "registry:ui"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
"path": "src/wc/components/PaperTexture/paper-texture-types.ts",
|
|
137
|
+
"content": "export type PaperTextureFit = 'none' | 'contain' | 'cover';\n\n/**\n * Literal parameters forwarded to Paper Design's paper texture shader.\n * Colors intentionally remain ordinary CSS color strings; they are not token\n * references and are parsed once into shader RGBA uniforms.\n */\nexport interface PaperTextureConfig {\n image?: HTMLImageElement | string;\n colorFront?: string;\n colorBack?: string;\n contrast?: number;\n roughness?: number;\n fiber?: number;\n fiberSize?: number;\n crumples?: number;\n crumpleSize?: number;\n folds?: number;\n foldCount?: number;\n fade?: number;\n drops?: number;\n seed?: number;\n fit?: PaperTextureFit;\n scale?: number;\n rotation?: number;\n originX?: number;\n originY?: number;\n offsetX?: number;\n offsetY?: number;\n worldWidth?: number;\n worldHeight?: number;\n speed?: number;\n frame?: number;\n minPixelRatio?: number;\n maxPixelCount?: number;\n /** CSS opacity applied after the shader has rendered. */\n opacity?: number;\n}\n\nexport const DEFAULT_PAPER_TEXTURE_CONFIG: Required<Omit<PaperTextureConfig, 'image'>> = {\n colorFront: '#b8b8b8',\n colorBack: '#ffffff',\n contrast: 0.3,\n roughness: 0.4,\n fiber: 0.3,\n fiberSize: 0.2,\n crumples: 0.3,\n crumpleSize: 0.35,\n folds: 0.65,\n foldCount: 5,\n fade: 0,\n drops: 0.2,\n seed: 5.8,\n fit: 'cover',\n scale: 0.6,\n rotation: 0,\n originX: 0.5,\n originY: 0.5,\n offsetX: 0,\n offsetY: 0,\n worldWidth: 0,\n worldHeight: 0,\n speed: 0,\n frame: 0,\n minPixelRatio: 2,\n maxPixelCount: 1920 * 1080 * 4,\n opacity: 1,\n};\n",
|
|
138
|
+
"type": "registry:ui"
|
|
139
|
+
}
|
|
140
|
+
]
|
|
141
|
+
}
|
|
@@ -5982,7 +5982,7 @@
|
|
|
5982
5982
|
"Page-size changes request the first page. Out-of-range page indexes and invalid numeric inputs normalize only for presentation; the owner remains authoritative.",
|
|
5983
5983
|
"Zero results display 0–0 of 0 and Page 1 of 1. A partial final page caps the visible range at totalItems.",
|
|
5984
5984
|
"loading preserves the full pagination surface, applies aria-busy to the navigation region, and prevents page or page-size requests.",
|
|
5985
|
-
"fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. Pagination does not measure layouts itself."
|
|
5985
|
+
"fitToPage appends a Fit to page choice. fitPageSize supplies its effective whole-item capacity; the closed trigger says Fit while the range continues to expose the resulting count. fitToPageInactive keeps that choice visible but unavailable when fitting is not meaningful for the current data shape. Pagination does not measure layouts itself."
|
|
5986
5986
|
],
|
|
5987
5987
|
"responsiveBehavior": [
|
|
5988
5988
|
"The compact controls remain on one line with noun, selected size, total, a vertical divider, and page position. When inline space is still insufficient, the pagination surface scrolls horizontally rather than wrapping labels away from their controls."
|
|
@@ -6022,6 +6022,10 @@
|
|
|
6022
6022
|
"type": "boolean",
|
|
6023
6023
|
"required": false
|
|
6024
6024
|
},
|
|
6025
|
+
"fitToPageInactive": {
|
|
6026
|
+
"type": "boolean",
|
|
6027
|
+
"required": false
|
|
6028
|
+
},
|
|
6025
6029
|
"itemLabel": {
|
|
6026
6030
|
"type": "string",
|
|
6027
6031
|
"required": false
|
|
@@ -6924,6 +6928,85 @@
|
|
|
6924
6928
|
"tooltip"
|
|
6925
6929
|
]
|
|
6926
6930
|
},
|
|
6931
|
+
{
|
|
6932
|
+
"name": "paper-texture",
|
|
6933
|
+
"title": "PaperTexture",
|
|
6934
|
+
"description": "A decorative Paper Design WebGL2 paper texture surface with Storybook-tunable literal shader parameters and a static default frame.",
|
|
6935
|
+
"type": "registry:ui",
|
|
6936
|
+
"meta": {
|
|
6937
|
+
"library": "@ds-mo/ui",
|
|
6938
|
+
"distribution": "npm",
|
|
6939
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/paper-texture",
|
|
6940
|
+
"source": "src/wc/components/PaperTexture/PaperTexture.tsx",
|
|
6941
|
+
"intentStatus": "complete",
|
|
6942
|
+
"intent": {
|
|
6943
|
+
"audience": "specialized",
|
|
6944
|
+
"status": "experimental",
|
|
6945
|
+
"summary": "A decorative Paper Design WebGL2 paper texture surface with Storybook-tunable literal shader parameters and a static default frame.",
|
|
6946
|
+
"useWhen": [
|
|
6947
|
+
"A shell or other visual surface needs a paper or cardboard texture layered above an existing background.",
|
|
6948
|
+
"Designers need to tune paper texture parameters interactively before choosing a baked static asset."
|
|
6949
|
+
],
|
|
6950
|
+
"avoidWhen": [
|
|
6951
|
+
"The surface needs semantic content, interaction, or accessible information.",
|
|
6952
|
+
"The target environment cannot provide WebGL2 and no pre-baked fallback asset is available.",
|
|
6953
|
+
"A token-driven color treatment is required; this component accepts literal shader colors only."
|
|
6954
|
+
],
|
|
6955
|
+
"alternatives": [],
|
|
6956
|
+
"commonlyComposedWith": [
|
|
6957
|
+
"component:ds-shell-app"
|
|
6958
|
+
],
|
|
6959
|
+
"accessibility": [
|
|
6960
|
+
"The texture is decorative and is hidden from assistive technology.",
|
|
6961
|
+
"Keep all meaningful content in the owning surface above or beside the texture."
|
|
6962
|
+
],
|
|
6963
|
+
"states": [
|
|
6964
|
+
"The default speed is zero, so a texture renders one deterministic frame without a recurring animation loop.",
|
|
6965
|
+
"Paper shader colors are literal CSS color strings and are not resolved from TokoMo tokens.",
|
|
6966
|
+
"If WebGL2 or the noise texture is unavailable, the decorative layer removes itself without blocking the owning surface."
|
|
6967
|
+
],
|
|
6968
|
+
"responsiveBehavior": [
|
|
6969
|
+
"The shader observes its host dimensions and follows the owning surface through resize changes.",
|
|
6970
|
+
"minPixelRatio and maxPixelCount are available for explicit quality/performance tuning."
|
|
6971
|
+
],
|
|
6972
|
+
"frameworkCaveats": {
|
|
6973
|
+
"customElements": [
|
|
6974
|
+
"Assign config as a JavaScript property; nested shader values are not intended for serialized HTML attributes."
|
|
6975
|
+
],
|
|
6976
|
+
"react": [
|
|
6977
|
+
"Use the generated wrapper and pass the config object as a property."
|
|
6978
|
+
],
|
|
6979
|
+
"angular": [
|
|
6980
|
+
"Bind the config object as a property so nested shader values are preserved."
|
|
6981
|
+
]
|
|
6982
|
+
},
|
|
6983
|
+
"references": [
|
|
6984
|
+
{
|
|
6985
|
+
"label": "Storybook parameter playground",
|
|
6986
|
+
"path": "src/wc/components/PaperTexture/PaperTexture.stories.ts"
|
|
6987
|
+
}
|
|
6988
|
+
]
|
|
6989
|
+
},
|
|
6990
|
+
"props": {
|
|
6991
|
+
"config": {
|
|
6992
|
+
"type": "PaperTextureConfig",
|
|
6993
|
+
"required": false
|
|
6994
|
+
}
|
|
6995
|
+
},
|
|
6996
|
+
"events": [],
|
|
6997
|
+
"slots": [],
|
|
6998
|
+
"exports": {
|
|
6999
|
+
"customElement": "ds-paper-texture",
|
|
7000
|
+
"react": "DsPaperTexture",
|
|
7001
|
+
"angular": "DsPaperTexture"
|
|
7002
|
+
}
|
|
7003
|
+
},
|
|
7004
|
+
"dependencies": [
|
|
7005
|
+
"@ds-mo/ui",
|
|
7006
|
+
"@ds-mo/tokens"
|
|
7007
|
+
],
|
|
7008
|
+
"registryDependencies": []
|
|
7009
|
+
},
|
|
6927
7010
|
{
|
|
6928
7011
|
"name": "radio",
|
|
6929
7012
|
"title": "Radio",
|
|
@@ -7511,7 +7594,8 @@
|
|
|
7511
7594
|
"component:ds-mobile-bar-nav",
|
|
7512
7595
|
"component:ds-shell-page",
|
|
7513
7596
|
"component:ds-banner",
|
|
7514
|
-
"component:ds-swatch-picker"
|
|
7597
|
+
"component:ds-swatch-picker",
|
|
7598
|
+
"component:ds-paper-texture"
|
|
7515
7599
|
],
|
|
7516
7600
|
"patterns": [
|
|
7517
7601
|
"pattern:application-shell",
|
|
@@ -7537,6 +7621,7 @@
|
|
|
7537
7621
|
"Use composition=slotted only for advanced application architectures that intentionally own all lower-level chrome wiring.",
|
|
7538
7622
|
"Set gradientPreset to the single resolved global chrome preference: neutral by default, none for solid chrome, or cool, neutral, warm, and fresh for token-based washes.",
|
|
7539
7623
|
"The application owns gradient preference persistence, cross-tab synchronization, and server or account precedence; ShellApp owns rendering the resolved preset across authenticated routes.",
|
|
7624
|
+
"The optional paperTexture property adds a decorative Paper Design texture layer above the gradient or solid chrome frame; its color values are literal shader inputs rather than design tokens.",
|
|
7540
7625
|
"Dashboard represents day-to-day operational work while Settings represents administrative and configuration work; route state selects the context.",
|
|
7541
7626
|
"Replace navigation and pageChrome together from one completed route state.",
|
|
7542
7627
|
"When pageChrome.showBack marks a detail depth, mobile chrome keeps the page heading as its identity and presents only local subsections; peer route tabs remain in desktop BarNav rather than replacing the detail heading.",
|
|
@@ -7557,20 +7642,24 @@
|
|
|
7557
7642
|
"A slotted Banner remains below the top safe-area inset and above routed, navigation, fullscreen-tool, and mobile global-surface presentations.",
|
|
7558
7643
|
"Mobile global surfaces conceal and inert the routed workspace without unmounting it, preserving nested routes and scroll state.",
|
|
7559
7644
|
"Keep PanelNav width, BarNav height, PanelTools chrome, and main-content origin stable except during intentional responsive or collapse transitions.",
|
|
7560
|
-
"
|
|
7645
|
+
"Paper texture chrome is desktop/tablet-only and follows the fixed viewport phase of the shell chrome; mobile remains a solid primary stage.",
|
|
7646
|
+
"Managed page-header capacity follows only the fixed shell breakpoint: roomy on desktop and compact on tablet. Opening PanelTools does not compact or constrain BarTitle.",
|
|
7561
7647
|
"The bottom bar remains visible at all route and tool depths in the foundation release."
|
|
7562
7648
|
],
|
|
7563
7649
|
"frameworkCaveats": {
|
|
7564
7650
|
"customElements": [
|
|
7565
7651
|
"Assign navigation, pageChrome, and tools as JavaScript properties on the managed ShellApp.",
|
|
7652
|
+
"Assign paperTexture as a JavaScript property when enabling the optional decorative layer.",
|
|
7566
7653
|
"Stamp route-derived nav-style before components connect when deterministic first paint is required."
|
|
7567
7654
|
],
|
|
7568
7655
|
"react": [
|
|
7569
7656
|
"Use the generated ShellApp wrapper, structured object props, named tool-view slots, and stable routed children.",
|
|
7657
|
+
"Pass paperTexture as a structured property when the shell needs the optional texture layer.",
|
|
7570
7658
|
"Resolve all shell inputs from one router state before rendering the composition."
|
|
7571
7659
|
],
|
|
7572
7660
|
"angular": [
|
|
7573
7661
|
"Use the generated standalone ShellApp adapter, bind structured object props, and preserve named tool-view slots.",
|
|
7662
|
+
"Bind paperTexture as a property so nested shader parameters are preserved.",
|
|
7574
7663
|
"Bind complex inputs as properties and update navigation inputs atomically from one resolved router state.",
|
|
7575
7664
|
"Bind both nav-style attributes and properties when deterministic first paint is required."
|
|
7576
7665
|
]
|
|
@@ -7623,6 +7712,10 @@
|
|
|
7623
7712
|
"type": "ShellPageChromeConfig",
|
|
7624
7713
|
"required": false
|
|
7625
7714
|
},
|
|
7715
|
+
"paperTexture": {
|
|
7716
|
+
"type": "PaperTextureConfig",
|
|
7717
|
+
"required": false
|
|
7718
|
+
},
|
|
7626
7719
|
"shortcutsEnabled": {
|
|
7627
7720
|
"type": "boolean",
|
|
7628
7721
|
"required": false
|
|
@@ -7709,6 +7802,7 @@
|
|
|
7709
7802
|
"mobile-header",
|
|
7710
7803
|
"mobile-sheet-nav",
|
|
7711
7804
|
"panel-nav",
|
|
7805
|
+
"paper-texture",
|
|
7712
7806
|
"shell-page",
|
|
7713
7807
|
"shell-tools",
|
|
7714
7808
|
"tooltip"
|
|
@@ -8877,7 +8971,7 @@
|
|
|
8877
8971
|
"accessibility": [
|
|
8878
8972
|
"The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty. The native caption remains visually hidden as the table's accessible name. When caption visibility is enabled, a 48px header bar renders as component chrome outside the horizontal scrollport. It defaults to a presentational caption title that does not duplicate the accessible name. Application-owned labeled controls supplied through the single named header slot replace that fallback and occupy the leading content width inside the component-owned 8px inset. When columnCustomizer is enabled, the table adds a trailing Preferences control named Customize table; otherwise the header slot receives the complete content width.",
|
|
8879
8973
|
"The data-mode switcher is the shared Menu of the table's supported infinite, pagination, and virtual modes. The switcher is the final caption-bar action; when the column customizer is also present, a decorative vertical divider separates the two controls. dataMode remains controlled, and the application responds to change intent by supplying the matching records, pagination state, loading behavior, and query consequences. Escape, dismissal, and selection return focus to the trigger.",
|
|
8880
|
-
"The column customizer is the shared Menu of switch rows, not a table-owned dialog. Data columns can be shown, hidden, and reordered.
|
|
8974
|
+
"The column customizer is the shared Menu of switch rows, not a table-owned dialog. Data columns can be shown, hidden, and reordered. Action columns and the selection checkbox never appear in the menu. The last remaining visible data column cannot be hidden, but it stays focusable so it can still be reordered. Escape and outside click restore focus to the trigger. Persistence stays in the application.",
|
|
8881
8975
|
"Sorting controls remain buttons inside column headers and expose the table's one controlled member-row sort through aria-sort. Grouping never adds a second sort indicator or header control; the application supplies groups in its fixed product order. Optional column help underlines the header label with the shared dotted decoration and shows ds-tooltip from that label; it does not add a second header control or replace the column name.",
|
|
8882
8976
|
"Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window. In virtual mode the supplied in-memory rows are that loaded set, including rows whose DOM is recycled. When grouping is active with multi-selection, each group section header also exposes a checkbox that selects or clears that group's selectable loaded rows.",
|
|
8883
8977
|
"Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection, action, overflow-menu, or primary-text link controls.",
|
|
@@ -8888,10 +8982,10 @@
|
|
|
8888
8982
|
"states": [
|
|
8889
8983
|
"Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, hiddenColumnIds, columnOrder, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns the leading slot's internal layout, controls, labels, filtering behavior, and action handling. Opting into dataModeSwitcher and columnCustomizer adds their trailing Menu triggers and requires caption visibility. When both are present, Customize table precedes a decorative divider and the data-mode trigger remains the final action. The table closes one trailing Menu before opening the other. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
|
|
8890
8984
|
"Grouping supports one level. The application supplies groups in a fixed product-defined order and supplies ordered member rows independently. Group section headers show an emphasized text-body-medium label followed by a middle dot and plain secondary loaded progress, for example Critical · 50 of 166. The loaded value comes from the supplied member rows and the total comes from totalCount. A localized countLabel total phrase remains available for the complete assistive progress announcement. Virtual mode shows totals only, for example Critical · 166, because there is no loaded window. Groups may declare an optional intent (brand, neutral, negative, warning, caution, positive) that paints an LTR gradient from the faint intent surface at the label edge into faint neutral at the trailing edge, plus a bold intent title color. Neutral groups remain solid faint neutral. Section separators are painted inside that viewport-width surface so translucent border tokens composite with each intent background. Internal sections keep their separators; a collapsed or empty section header yields its separator only when it is the table's final rendered item. A trailing visually neutral icon-only ButtonUnfilled uses the 24px md double-inset action recipe and toggles controlled collapsedGroupIds, showing ChevronUp while expanded and ChevronDown while collapsed. While any group is expanded, a ChevronDownUp action uses the xs sort-control recipe widened to a table-owned 24px lane and collapses every group. It appears at the inline-end edge of the trailing column declared with kind action when one exists; otherwise a button-hugging 24px by 16px medium-elevation surface floats at the visible scrollport's header edge so the action remains reachable while member columns scroll beneath it. With stickyHeader enabled, each contained section uses its one real section row as a native sticky header inside its real row-group boundary. The browser therefore owns pinning, push-off, scrolling, and resize layout in one pass without a duplicated lane, active-section state, transforms, or scroll-time geometry. Shared grid tracks retain exact header and member-cell alignment. The section surface also stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width. Virtual grouped tables flatten headers and members into that same scrollport; expanding a section never introduces an inner scroller.",
|
|
8891
|
-
"When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenColumnIds and columnOrder control visibility and data-column sequence, action columns stay last
|
|
8985
|
+
"When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenColumnIds and columnOrder control visibility and data-column sequence, action columns stay last, cannot be hidden, and remain absent from the customizer Menu, and dsColumnsConfigChange reports the next hidden ids and data-column order without mutating those inputs. The customizer Menu live-applies show/hide and data-column reorder. The table does not persist preferences.",
|
|
8892
8986
|
"An active member sort toggles only between ascending and descending; activating a different sortable label moves the one sorting state to that key. Applications may still clear the controlled sort state explicitly. Grouping does not introduce another interactive direction.",
|
|
8893
8987
|
"Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
|
|
8894
|
-
"Initial loading defaults to ten row-shaped skeletons and preserves row dividers, column dividers, selection lanes, and sticky boundaries. A selectable table renders a checkbox-sized control skeleton in its selection lane so loading geometry matches the real checkbox instead of a generic icon. Each column may declare representative text, multiline text, image, Tag, icon, icon-and-text, action, or intentionally blank loading geometry; the table applies the matching resolved-cell padding and track anatomy so the placeholder row has the same height and rhythm as its data row. Columns without a declaration use one text line, while action columns infer an icon action placeholder. Empty and initial error replace only the body with ds-empty-state, using Inbox or ErrorTriangle plus the table heading and body copy. In height-bounded and maxHeight tables that region fills the remaining viewport below the column header; content-sized tables keep a minimum empty-region height. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
|
|
8988
|
+
"Initial loading defaults to ten row-shaped skeletons and preserves row dividers, column dividers, selection lanes, and sticky boundaries. A selectable table renders a checkbox-sized control skeleton in its selection lane so loading geometry matches the real checkbox instead of a generic icon. Each column may declare representative text, multiline text, image, Tag, icon, icon-and-text, action, or intentionally blank loading geometry; the table applies the matching resolved-cell padding and track anatomy so the placeholder row has the same height and rhythm as its data row. Columns without a declaration use one text line, while action columns infer an icon action placeholder. chromeLoading replaces opted-in table-owned caption controls with non-interactive control skeletons in the same slots and at the same dimensions, without changing dataMode or the surrounding caption geometry. Empty and initial error replace only the body with ds-empty-state, using Inbox or ErrorTriangle plus the table heading and body copy. In height-bounded and maxHeight tables that region fills the remaining viewport below the column header; content-sized tables keep a minimum empty-region height. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
|
|
8895
8989
|
"dataMode selects the top-level window: infinite appends ungrouped rows through dsLoadMore when hasMore is true, pagination renders the controlled page supplied by the application and emits dsPaginationChange, and virtual recycles row DOM over the full in-memory rows or groups the application already supplied. Infinite mode also covers fixed result sets by leaving hasMore false. In a grouped paginated table, pagination counts and windows only parent groups; each expanded parent can still append its own members through dsGroupLoadMore. Virtual mode does not fetch, ignores hasMore and load bands, and requires a bounded viewport (height, maxHeight, or fitViewport with a definite block size). Without that height it fails visibly instead of mounting every row. The application owns requests, cancellation, cursors, deduplication, slicing, and query identity.",
|
|
8896
8990
|
"When pagination enables Fit to page on a bounded table, the table proposes the number of complete top-level rows that fit below its column header. Grouped tables count group headers only; member rows never affect capacity. Selecting Fit captures that numeric snapshot. A changed fitIdentity requests one fresh snapshot for a new query shape, while viewport resizing alone does not change the active controlled page size.",
|
|
8897
8991
|
"Rows opt into complete-row activation with interactive and emit dsRowActivate without owning navigation. Selection paints through the shared selected ::before layer; interactive hover and press paint through the shared ::after layer.",
|
|
@@ -8951,6 +9045,10 @@
|
|
|
8951
9045
|
"type": "TableCaptionVisibility",
|
|
8952
9046
|
"required": false
|
|
8953
9047
|
},
|
|
9048
|
+
"chromeLoading": {
|
|
9049
|
+
"type": "boolean",
|
|
9050
|
+
"required": false
|
|
9051
|
+
},
|
|
8954
9052
|
"collapsedGroupIds": {
|
|
8955
9053
|
"type": "string[]",
|
|
8956
9054
|
"required": false
|
|
@@ -9280,12 +9378,12 @@
|
|
|
9280
9378
|
"The naming dialog traps focus through Modal behavior, associates validation text with the input through Field, and restores focus to the saved-views control after closing."
|
|
9281
9379
|
],
|
|
9282
9380
|
"states": [
|
|
9283
|
-
"views, value, and dirty are controlled. Selection, create, save, discard, rename, and remove events communicate intent and never mutate application records.",
|
|
9381
|
+
"views, value, and dirty are controlled. Dirty state is presented only for a selected custom view; the default template never shows the trigger dot or save/discard actions even if dirty is supplied. Selection, create, save, discard, rename, and remove events communicate intent and never mutate application records.",
|
|
9284
9382
|
"The component owns only transient interaction state: the contextual menu, naming dialog, draft name, duplicate-name validation, and focus restoration.",
|
|
9285
9383
|
"The default view is a non-editable template. Application-owned custom views appear in a separate section with Rename and Remove actions. New view opens the naming dialog. When an editable selected view is dirty, that view row shows Save and negative Discard supporting actions while the footer remains dedicated to New view. The application decides which table state to capture, restore, and persist."
|
|
9286
9384
|
],
|
|
9287
9385
|
"responsiveBehavior": [
|
|
9288
|
-
"The control remains a single toolbar item and relies on Select, Menu, and Modal collision and viewport behavior.",
|
|
9386
|
+
"The control remains a single toolbar item and relies on Select, Menu, and Modal collision and viewport behavior. Its choice popup keeps the token-backed 200px menu-width-xs minimum.",
|
|
9289
9387
|
"Place it in the leading region of ds-table-toolbar so narrow table headers preserve the expected control order."
|
|
9290
9388
|
],
|
|
9291
9389
|
"frameworkCaveats": {
|
|
@@ -925,12 +925,12 @@
|
|
|
925
925
|
"files": [
|
|
926
926
|
{
|
|
927
927
|
"path": "src/wc/components/Select/Select.css",
|
|
928
|
-
"content": "@import '../../utils/choice-list.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/search-control.css';\n@import '../../utils/select-trigger.css';\n@import '../../utils/typography.css';\n@import '../../styles/control-elevation.css';\n\n.wrapper--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.select-popup {\n color: var(--color-foreground-secondary);\n}\n\n.select-option[aria-selected='true'] .ds-choice-item__icon {\n color: var(--color-foreground-primary);\n}\n\n.select-option-row {\n position: relative;\n isolation: isolate;\n}\n\n.select-option-row__action {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-050);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.select-option-row:has(:focus-visible) .select-option-row__action,\n.select-option-row--action-open .select-option-row__action,\n.select-option-row--keyboard-active .select-option-row__action {\n opacity: 1;\n pointer-events: auto;\n}\n\n.select-option-row--action-open .select-option::after {\n background: var(--ds-interaction-hover);\n}\n\n.select-option__subtext-actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-
|
|
928
|
+
"content": "@import '../../utils/choice-list.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/search-control.css';\n@import '../../utils/select-trigger.css';\n@import '../../utils/typography.css';\n@import '../../styles/control-elevation.css';\n\n.wrapper--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.select-popup {\n color: var(--color-foreground-secondary);\n}\n\n.select-option[aria-selected='true'] .ds-choice-item__icon {\n color: var(--color-foreground-primary);\n}\n\n.select-option-row {\n position: relative;\n isolation: isolate;\n}\n\n.select-option-row__action {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-050);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.select-option-row:has(:focus-visible) .select-option-row__action,\n.select-option-row--action-open .select-option-row__action,\n.select-option-row--keyboard-active .select-option-row__action {\n opacity: 1;\n pointer-events: auto;\n}\n\n.select-option-row--action-open .select-option::after {\n background: var(--ds-interaction-hover);\n}\n\n.select-option__subtext-actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-inline-size: 0;\n color: var(--color-foreground-secondary);\n}\n\n.select-option__subtext-action {\n appearance: none;\n display: inline-flex;\n align-items: center;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-025);\n background: transparent;\n cursor: pointer;\n}\n\n.select-option__subtext-action--negative {\n --ds-text-action-foreground: var(--color-foreground-bold-negative);\n}\n\n.select-option__subtext-action-separator {\n color: var(--color-foreground-tertiary);\n pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .select-option-row:hover .select-option-row__action {\n opacity: 1;\n pointer-events: auto;\n }\n\n .select-option-row:hover .select-option::after {\n background: var(--ds-interaction-hover);\n }\n}\n\n@media (hover: none) {\n .select-option-row__action {\n opacity: 1;\n pointer-events: auto;\n }\n\n .select-option-row--has-action .ds-choice-item__content {\n padding-inline-end: var(--dimension-size-400);\n }\n}\n\n.error-text {\n width: 100%;\n}\n",
|
|
929
929
|
"type": "registry:ui"
|
|
930
930
|
},
|
|
931
931
|
{
|
|
932
932
|
"path": "src/wc/components/Select/Select.tsx",
|
|
933
|
-
"content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\nimport {\n ChoiceActionFooter,\n ChoiceFooter,\n ChoiceOptionRow,\n ChoiceSearch,\n} from '../../utils/choice-list-parts';\nimport {\n choiceBackgroundClassMap,\n choiceListUsesIcons,\n choiceListUsesSubtext,\n enabledChoiceIndexes,\n filterChoiceSections,\n flattenChoiceSections,\n resolveChoiceSections,\n type ChoiceBackground,\n type ChoiceOption,\n type ChoiceSection,\n} from '../../utils/choice-list';\nimport { SelectController } from '../../utils/select-controller';\n\nexport type SelectOption = ChoiceOption;\nexport type SelectSection = ChoiceSection;\nexport type SelectBackground = ChoiceBackground;\nexport type SelectSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type SelectWidth = ControlWidth;\nexport type SelectPopupAlign = 'start' | 'end';\nexport type SelectValue = string | string[];\nexport type SelectIndicator = 'down' | 'up-down';\n\nexport interface SelectOptionActionDetail {\n value: string;\n anchorId: string;\n originalEvent: MouseEvent;\n}\n\nexport interface SelectOptionSubtextActionDetail {\n value: string;\n actionValue: string;\n originalEvent: MouseEvent;\n}\n\nconst ICON_SIZE: Record<SelectSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst OPTION_ACTION_SIZE: Record<SelectSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet selectId = 0;\n\n@Component({\n tag: 'ds-select',\n styleUrl: 'Select.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Select {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Flat choices. Assign arrays through the JavaScript property. */\n @Prop() options: SelectOption[] = [];\n /** Grouped choices; takes precedence over options. Assign through JavaScript. */\n @Prop() sections: SelectSection[] = [];\n /** Enable independent multi-value selection while keeping the popup open. */\n @Prop({ reflect: true }) multiple: boolean = false;\n /** Selected scalar or array value according to `multiple`. */\n @Prop({ mutable: true }) value: SelectValue = '';\n /** Controlled popup visibility. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one valid selected value. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when required is missing. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Trigger text shown when no valid value is selected. */\n @Prop() placeholder: string = 'Select';\n /** Optional scalar-mode trigger text that does not change the selected option label. */\n @Prop() triggerLabel: string | undefined;\n /** Present triggerLabel with placeholder emphasis while preserving the selected value. */\n @Prop() triggerLabelPlaceholder: boolean = false;\n /** Show a supplemental notification dot beside the trigger label. */\n @Prop() dot: boolean = false;\n /** Control density. */\n @Prop() size: SelectSize = 'md';\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: SelectWidth = 'hug';\n /** Align the popup's choice edge to the trigger start or end edge. */\n @Prop() popupAlign: SelectPopupAlign = 'start';\n /** Shared inactive treatment; removes interaction and form submission. */\n @Prop() isInactive: boolean = false;\n /** Replace the prefix with a loader and disable option interaction. */\n @Prop() isLoading: boolean = false;\n /** Show the selected interaction fill when a valid value exists. */\n @Prop() activeFill: boolean = true;\n /** Show the surface-aware inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional trigger prefix icon name. */\n @Prop() icon: string | undefined;\n /** Trailing choice indicator. Use up-down for compact value steppers such as page size. */\n @Prop() indicator: SelectIndicator = 'down';\n /** Show the clear footer action when a value exists. */\n @Prop() allowClear: boolean = true;\n /** Localized clear action label. */\n @Prop() clearLabel: string = 'Clear';\n /** Localized noun displayed after the selected count in multiple mode. */\n @Prop() selectedLabel: string = 'selected';\n /** Optional text action shown in the popup footer instead of the clear action. */\n @Prop() footerActionLabel: string | undefined;\n /** Show immediate local filtering over option labels, subtext, and section headings. */\n @Prop() searchable: boolean = false;\n /** Localized search-field placeholder and accessible name. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Localized empty-filter result text. */\n @Prop() noResultsText: string = 'No results found';\n /** Accessible loading status label. */\n @Prop() loadingLabel: string = 'Loading';\n /** Actual parent surface context; omit on primary and secondary surfaces. */\n @Prop() background: SelectBackground | undefined;\n /** Show invalid visual state. */\n @Prop() error: boolean = false;\n /** Error text rendered below the trigger when error is true. */\n @Prop() errorMessage: string | undefined;\n /** ID applied to the internal combobox trigger for external labels. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name when no external label is available. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** IDs of elements that label the combobox. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Additional IDs that describe the combobox. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n /** Emitted after selection or clearing with the next scalar or array value. */\n @Event() dsChange!: EventEmitter<SelectValue>;\n /** Emitted after the footer clear action. */\n @Event() dsClear!: EventEmitter<void>;\n /** Emitted whenever popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted when the optional popup footer action is activated. */\n @Event() dsFooterAction!: EventEmitter<void>;\n /** Emitted when an option's contextual ellipsis action is activated. */\n @Event() dsOptionAction!: EventEmitter<SelectOptionActionDetail>;\n /** Emitted when an option's supporting text action is activated. */\n @Event() dsOptionSubtextAction!: EventEmitter<SelectOptionSubtextActionDetail>;\n\n @State() private activeIndex = -1;\n @State() private searchTerm = '';\n @State() private formDisabled = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private focusRingVisible = false;\n @State() private compactFooterSummary = false;\n\n private readonly generatedId = `ds-select-${++selectId}`;\n private readonly listboxId = `${this.generatedId}-listbox`;\n private readonly errorId = `${this.generatedId}-error`;\n private initialValue: SelectValue = '';\n private readonly controller = this.createController();\n private footerContentElement?: HTMLDivElement;\n private footerSummaryMeasureElement?: HTMLElement;\n private footerClearElement?: HTMLButtonElement;\n private footerResizeObserver?: ResizeObserver;\n private observedFooterContentElement?: HTMLDivElement;\n private footerMeasurementFrame?: number;\n\n private createController() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- adapter getters preserve reactive component ownership without inheritance.\n const owner = this;\n return new SelectController<SelectOption>({\n get host() {\n return owner.el;\n },\n generatedId: owner.generatedId,\n get options() {\n return owner.visibleOptions;\n },\n get searchable() {\n return owner.searchable;\n },\n get isLoading() {\n return owner.isLoading;\n },\n get isDisabled() {\n return owner.isDisabled;\n },\n get preferredIndex() {\n if (owner.multiple) return -1;\n return owner.visibleOptions.findIndex(\n option => option.value === owner.scalarValue && !option.isInactive\n );\n },\n get popupAlign() {\n return owner.popupAlign;\n },\n get open() {\n return owner.open;\n },\n set open(value) {\n owner.open = value;\n },\n get activeIndex() {\n return owner.activeIndex;\n },\n set activeIndex(value) {\n owner.activeIndex = value;\n },\n get searchTerm() {\n return owner.searchTerm;\n },\n set searchTerm(value) {\n owner.searchTerm = value;\n },\n get focusRingVisible() {\n return owner.focusRingVisible;\n },\n set focusRingVisible(value) {\n owner.focusRingVisible = value;\n },\n get position() {\n return owner.position;\n },\n set position(value) {\n owner.position = value;\n },\n get positionReady() {\n return owner.positionReady;\n },\n set positionReady(value) {\n owner.positionReady = value;\n },\n selectOption: option => owner.selectOption(option),\n });\n }\n\n componentWillLoad() {\n this.value = this.normalizeValueForMode(this.value);\n this.initialValue = Array.isArray(this.value) ? [...this.value] : this.value;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.controller.connect();\n this.observeFooterContent();\n }\n\n componentDidRender() {\n this.observeFooterContent();\n this.scheduleFooterSummaryMeasurement();\n }\n\n disconnectedCallback() {\n this.controller.disconnect();\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n if (this.footerMeasurementFrame !== undefined) {\n cancelAnimationFrame(this.footerMeasurementFrame);\n }\n }\n\n @Watch('value')\n @Watch('multiple')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('name')\n @Watch('options')\n @Watch('sections')\n syncFormValue() {\n if (\n (this.multiple && !Array.isArray(this.value)) ||\n (!this.multiple && Array.isArray(this.value))\n ) {\n this.value = this.normalizeValueForMode(this.value);\n }\n const inactive = this.isDisabled;\n if (this.multiple) {\n const values = this.resolvedValues;\n setRepeatedFormControlValue(this.internals, this.name, values, { inactive });\n } else {\n const value = this.hasSelection ? this.scalarValue : '';\n setFormControlValue(this.internals, value, { inactive });\n }\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n this.controller.optionsChanged();\n }\n\n @Watch('isLoading')\n onLoadingChange() {\n this.controller.loadingChanged();\n }\n\n @Watch('popupAlign')\n onPopupAlignChange() {\n this.controller.positionChanged();\n }\n\n @Watch('open')\n onOpenChange(open: boolean) {\n this.dsOpenChange.emit(open);\n this.controller.openChanged(open);\n }\n\n @Watch('searchTerm')\n onSearchTermChange() {\n this.controller.searchChanged();\n }\n\n @Watch('activeIndex')\n onActiveIndexChange() {\n this.controller.activeIndexChanged();\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.normalizeValueForMode(this.initialValue);\n this.closePopup();\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (!this.multiple) {\n this.value = restoreStringFormState(state);\n return;\n }\n this.value = restoreStringArrayFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.controller.setFocus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private normalizeValueForMode(value: SelectValue): SelectValue {\n if (this.multiple) {\n return Array.isArray(value) ? [...value] : value ? [value] : [];\n }\n return Array.isArray(value) ? value[0] ?? '' : value;\n }\n\n private get allSections(): SelectSection[] {\n return resolveChoiceSections(this.options, this.sections);\n }\n\n private get allOptions(): SelectOption[] {\n return flattenChoiceSections(this.allSections);\n }\n\n private get visibleSections(): SelectSection[] {\n return this.searchable\n ? filterChoiceSections(this.allSections, this.searchTerm)\n : this.allSections;\n }\n\n private get visibleOptions(): SelectOption[] {\n return flattenChoiceSections(this.visibleSections);\n }\n\n private get scalarValue(): string {\n return typeof this.value === 'string' ? this.value : '';\n }\n\n private get resolvedValues(): string[] {\n if (!this.multiple || !Array.isArray(this.value)) return [];\n const valid = new Set(this.allOptions.map(option => option.value));\n return [...new Set(this.value.filter(value => valid.has(value)))];\n }\n\n private get selectedOption(): SelectOption | undefined {\n if (this.multiple) return undefined;\n return this.allOptions.find(option => option.value === this.scalarValue);\n }\n\n private get hasSelection(): boolean {\n return this.multiple ? this.resolvedValues.length > 0 : Boolean(this.selectedOption);\n }\n\n private get activeOptionId(): string | undefined {\n return this.controller.activeOptionId;\n }\n\n private get popupRole(): 'listbox' | 'grid' {\n return this.allOptions.some(option => Boolean(option.action)) ? 'grid' : 'listbox';\n }\n\n private observeFooterContent() {\n const content =\n this.open && this.multiple && this.hasSelection\n ? this.footerContentElement\n : undefined;\n if (!content?.isConnected) {\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n return;\n }\n if (this.observedFooterContentElement === content) return;\n\n this.footerResizeObserver?.disconnect();\n if (typeof ResizeObserver !== 'undefined') {\n this.footerResizeObserver = new ResizeObserver(() =>\n this.scheduleFooterSummaryMeasurement()\n );\n this.footerResizeObserver.observe(content);\n }\n this.observedFooterContentElement = content;\n }\n\n private scheduleFooterSummaryMeasurement() {\n if (\n !this.open ||\n !this.multiple ||\n !this.hasSelection ||\n this.footerMeasurementFrame !== undefined\n ) {\n return;\n }\n this.footerMeasurementFrame = requestAnimationFrame(() => {\n this.footerMeasurementFrame = undefined;\n this.measureFooterSummary();\n });\n }\n\n private measureFooterSummary() {\n const content = this.footerContentElement;\n const summary = this.footerSummaryMeasureElement;\n const clear = this.footerClearElement;\n if (!content?.isConnected || !summary?.isConnected || !clear?.isConnected) return;\n\n const styles = getComputedStyle(content);\n const innerWidth =\n content.clientWidth -\n Number.parseFloat(styles.paddingInlineStart || '0') -\n Number.parseFloat(styles.paddingInlineEnd || '0');\n const requiredWidth =\n summary.getBoundingClientRect().width +\n clear.getBoundingClientRect().width +\n Number.parseFloat(styles.columnGap || '0');\n const nextCompact = requiredWidth > innerWidth + 0.5;\n if (nextCompact !== this.compactFooterSummary) {\n this.compactFooterSummary = nextCompact;\n }\n }\n\n private closePopup(restoreFocus = false) {\n this.controller.closePopup(restoreFocus);\n }\n\n private openPopup(focusVisible: boolean, edge?: 'first' | 'last') {\n this.controller.openPopup(focusVisible, edge);\n }\n\n private selectOption(option: SelectOption) {\n if (option.isInactive || this.isLoading) return;\n if (this.multiple) {\n const current = this.resolvedValues;\n const next = current.includes(option.value)\n ? current.filter(value => value !== option.value)\n : [...current, option.value];\n this.value = next;\n this.dsChange.emit([...next]);\n return;\n }\n this.value = option.value;\n this.dsChange.emit(option.value);\n this.controller.closePopup(true);\n }\n\n private clearSelection = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n this.value = this.multiple ? [] : '';\n this.dsChange.emit(this.multiple ? [] : '');\n this.dsClear.emit();\n if (!this.multiple) {\n const enabled = enabledChoiceIndexes(this.visibleOptions);\n this.activeIndex = enabled[0] ?? -1;\n }\n this.controller.focusSearchOrTrigger();\n };\n\n private activateFooterAction = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsFooterAction.emit();\n this.closePopup();\n };\n\n private readonly handleListKeyDown = this.controller.handleListKeyDown;\n private readonly handleTriggerKeyDown = this.controller.handleTriggerKeyDown;\n\n private renderOption(\n option: SelectOption,\n index: number,\n usesIcons: boolean,\n usesSubtext: boolean\n ) {\n const selected = this.multiple\n ? this.resolvedValues.includes(option.value)\n : option.value === this.scalarValue;\n const active = index === this.activeIndex;\n const actionId = `${this.generatedId}-option-action-${index}`;\n return (\n <ChoiceOptionRow\n size={this.size}\n id={`${this.generatedId}-option-${index}`}\n option={option}\n selected={selected}\n active={active}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n actionOpen={option.action?.expanded}\n popupRole={this.popupRole}\n leading={\n this.multiple ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox\n class=\"select-option__checkbox\"\n label=\"\"\n size={this.size}\n checked={selected}\n presentation\n />\n </span>\n ) : usesIcons && option.icon ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={option.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : undefined\n }\n supporting={\n option.subtextActions?.length ? (\n <span class=\"ds-choice-item__subtext ds-control-label-box select-option__subtext-actions\">\n {option.subtextActions.map((action, actionIndex) => [\n actionIndex > 0 ? (\n <ds-text\n class=\"select-option__subtext-action-separator\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null,\n <button\n type=\"button\"\n class={{\n 'select-option__subtext-action': true,\n 'select-option__subtext-action--negative': action.tone === 'negative',\n 'ds-text-action': true,\n 'ds-focus-ring': true,\n }}\n onKeyDown={(event: KeyboardEvent) => event.stopPropagation()}\n onClick={(event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsOptionSubtextAction.emit({\n value: option.value,\n actionValue: action.value,\n originalEvent: event,\n });\n }}\n >\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n >\n {action.label}\n </ds-text>\n </button>,\n ])}\n </span>\n ) : undefined\n }\n action={\n option.action ? (\n <ds-tooltip label={option.action.label} side=\"left\" size=\"sm\">\n <ds-button-unfilled\n id={actionId}\n variant=\"icon\"\n size={OPTION_ACTION_SIZE[this.size]}\n icon=\"Ellipses\"\n rounded\n hasBorder={false}\n activeFill={false}\n ariaLabel={option.action.label}\n haspopup=\"menu\"\n controls={option.action.controls}\n expanded={option.action.expanded}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.key === 'Escape' || event.key === 'ArrowLeft') {\n event.preventDefault();\n this.controller.focusSearchOrTrigger();\n }\n }}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsOptionAction.emit({\n value: option.value,\n anchorId: actionId,\n originalEvent: event.detail,\n });\n }}\n />\n </ds-tooltip>\n ) : undefined\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onSelect={() => this.selectOption(option)}\n />\n );\n }\n\n render() {\n const inactive = this.isDisabled;\n const showPlaceholder = !this.hasSelection;\n const count = this.resolvedValues.length;\n const label = this.multiple\n ? `${this.placeholder}${count > 0 ? ` · ${count}` : ''}`\n : showPlaceholder\n ? this.placeholder\n : (this.triggerLabel ?? this.selectedOption?.label);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const usesOptionIcons = !this.multiple && choiceListUsesIcons(this.allOptions);\n const usesOptionSubtext = choiceListUsesSubtext(this.allOptions);\n const showError = this.error && Boolean(this.errorMessage);\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n let flatIndex = 0;\n\n return (\n <Host\n class={{\n 'select-host': true,\n 'ds-select-trigger-host': true,\n 'select-host--multiple': this.multiple,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n [`select-host--background-${this.background}`]: !!this.background,\n [`ds-select-trigger-host--background-${this.background}`]: !!this.background,\n }}\n >\n <button\n ref={element => {\n this.controller.setTriggerElement((element as HTMLButtonElement) ?? null);\n }}\n id={this.inputId ?? this.generatedId}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': !inactive && this.activeFill && this.hasSelection,\n 'trigger--expanded': !inactive && this.open,\n 'trigger--bordered': this.hasBorder,\n 'trigger--placeholder': showPlaceholder && !this.multiple,\n 'trigger--has-value': this.hasSelection,\n 'trigger--label-placeholder':\n !this.multiple && Boolean(this.triggerLabel) && this.triggerLabelPlaceholder,\n 'wrapper--error': this.hasBorder && this.error,\n [`ds-control--${this.size}`]: true,\n ...choiceBackgroundClassMap(this.background),\n }}\n disabled={inactive}\n role=\"combobox\"\n aria-haspopup={this.popupRole}\n aria-expanded={String(this.open)}\n aria-controls={this.open ? this.listboxId : undefined}\n aria-activedescendant={this.open && !this.searchable ? this.activeOptionId : undefined}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n aria-required={this.required || undefined}\n aria-busy={this.isLoading ? 'true' : undefined}\n onClick={() => (this.open ? this.closePopup() : this.openPopup(false))}\n onKeyDown={event => {\n const action = this.visibleOptions[this.activeIndex]?.action;\n if (\n this.open &&\n action &&\n (event.key === 'ArrowRight' || (event.key === 'F10' && event.shiftKey))\n ) {\n event.preventDefault();\n document\n .getElementById(`${this.generatedId}-option-action-${this.activeIndex}`)\n ?.querySelector<HTMLElement>('button')\n ?.focus();\n return;\n }\n this.handleTriggerKeyDown(event);\n }}\n >\n {(this.icon || this.isLoading) && (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n {this.isLoading ? (\n <ds-loader size={iconSize} color=\"inherit\" />\n ) : (\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n )}\n </span>\n )}\n <span\n class={{\n 'trigger__label-box': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <span\n class={{\n 'trigger__label-content': true,\n 'ds-control-label-box': true,\n 'ds-control-label-dot': this.dot,\n 'trigger__label-content--dot': this.dot,\n }}\n >\n <ds-text\n class=\"trigger__label\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n {this.dot && (\n <ds-badge\n class=\"trigger__dot ds-control-label-dot__badge\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </span>\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon\n name={this.indicator === 'up-down' ? 'ChevronUpDown' : 'ChevronDown'}\n size={iconSize}\n color=\"inherit\"\n />\n </span>\n </button>\n\n {this.open && (\n <div\n popover=\"manual\"\n ref={element => {\n this.controller.setPopupElement((element as HTMLDivElement) ?? null);\n }}\n class=\"select-popup ds-choice-popup\"\n style={popupStyle}\n >\n {this.searchable && (\n <ChoiceSearch\n size={this.size}\n value={this.searchTerm}\n placeholder={this.searchPlaceholder}\n controls={this.listboxId}\n activeDescendant={this.activeOptionId}\n inputRef={this.controller.setSearchElement}\n clearLabel={this.clearLabel}\n onValueChange={value => {\n this.searchTerm = value;\n }}\n onKeyDown={event => this.handleListKeyDown(event)}\n />\n )}\n <div\n id={this.listboxId}\n class=\"ds-choice-list\"\n role={this.popupRole}\n aria-multiselectable={this.multiple ? 'true' : undefined}\n aria-label={this.ariaLabel ?? this.placeholder}\n aria-busy={this.isLoading ? 'true' : undefined}\n >\n {this.isLoading ? (\n <div\n class=\"ds-choice-loading ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.loadingLabel}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-loader size={this.size} color=\"inherit\" />\n </span>\n ) : (\n <ds-loader size={this.size} color=\"inherit\" />\n )}\n </div>\n ) : this.visibleOptions.length === 0 ? (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-empty-state body={this.noResultsText} />\n </span>\n ) : (\n <ds-empty-state body={this.noResultsText} />\n )}\n </div>\n ) : (\n this.visibleSections.map((section, sectionIndex) => (\n <div\n class={{\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': !!section.divider,\n 'ds-choice-section--headed-after-first':\n sectionIndex > 0 && Boolean(section.header),\n }}\n role={\n this.popupRole === 'grid'\n ? 'rowgroup'\n : section.header\n ? 'group'\n : undefined\n }\n aria-label={section.header}\n >\n {section.header && this.popupRole === 'grid' ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n role=\"row\"\n >\n <span\n class=\"ds-choice-section__header-label\"\n role=\"gridcell\"\n >\n {section.header}\n </span>\n </ds-text>\n ) : section.header ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\n ) : null}\n {section.options.map(option =>\n this.renderOption(option, flatIndex++, usesOptionIcons, usesOptionSubtext)\n )}\n </div>\n ))\n )}\n </div>\n {this.footerActionLabel && !this.isLoading ? (\n <ChoiceActionFooter\n size={this.size}\n label={this.footerActionLabel}\n onAction={this.activateFooterAction}\n />\n ) : this.allowClear && this.hasSelection && !this.isLoading ? (\n <ChoiceFooter\n size={this.size}\n summary={\n this.multiple ? `${count} ${this.selectedLabel}` : undefined\n }\n compactSummary={this.multiple ? String(count) : undefined}\n useCompactSummary={this.compactFooterSummary}\n contentRef={element => {\n this.footerContentElement = element ?? undefined;\n }}\n summaryMeasureRef={element => {\n this.footerSummaryMeasureElement = element ?? undefined;\n }}\n clearRef={element => {\n this.footerClearElement = element ?? undefined;\n }}\n clearLabel={this.clearLabel}\n onClear={this.clearSelection}\n />\n ) : null}\n </div>\n )}\n\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
933
|
+
"content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\nimport {\n ChoiceActionFooter,\n ChoiceFooter,\n ChoiceOptionRow,\n ChoiceSearch,\n} from '../../utils/choice-list-parts';\nimport {\n choiceBackgroundClassMap,\n choiceListUsesIcons,\n choiceListUsesSubtext,\n enabledChoiceIndexes,\n filterChoiceSections,\n flattenChoiceSections,\n resolveChoiceSections,\n type ChoiceBackground,\n type ChoiceOption,\n type ChoiceSection,\n} from '../../utils/choice-list';\nimport { SelectController } from '../../utils/select-controller';\n\nexport type SelectOption = ChoiceOption;\nexport type SelectSection = ChoiceSection;\nexport type SelectBackground = ChoiceBackground;\nexport type SelectSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type SelectWidth = ControlWidth;\nexport type SelectPopupAlign = 'start' | 'end';\nexport type SelectValue = string | string[];\nexport type SelectIndicator = 'down' | 'up-down';\n\nexport interface SelectOptionActionDetail {\n value: string;\n anchorId: string;\n originalEvent: MouseEvent;\n}\n\nexport interface SelectOptionSubtextActionDetail {\n value: string;\n actionValue: string;\n originalEvent: MouseEvent;\n}\n\nconst ICON_SIZE: Record<SelectSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst OPTION_ACTION_SIZE: Record<SelectSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet selectId = 0;\n\n@Component({\n tag: 'ds-select',\n styleUrl: 'Select.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Select {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Flat choices. Assign arrays through the JavaScript property. */\n @Prop() options: SelectOption[] = [];\n /** Grouped choices; takes precedence over options. Assign through JavaScript. */\n @Prop() sections: SelectSection[] = [];\n /** Enable independent multi-value selection while keeping the popup open. */\n @Prop({ reflect: true }) multiple: boolean = false;\n /** Selected scalar or array value according to `multiple`. */\n @Prop({ mutable: true }) value: SelectValue = '';\n /** Controlled popup visibility. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one valid selected value. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when required is missing. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Trigger text shown when no valid value is selected. */\n @Prop() placeholder: string = 'Select';\n /** Optional scalar-mode trigger text that does not change the selected option label. */\n @Prop() triggerLabel: string | undefined;\n /** Present triggerLabel with placeholder emphasis while preserving the selected value. */\n @Prop() triggerLabelPlaceholder: boolean = false;\n /** Show a supplemental notification dot beside the trigger label. */\n @Prop() dot: boolean = false;\n /** Control density. */\n @Prop() size: SelectSize = 'md';\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: SelectWidth = 'hug';\n /** Align the popup's choice edge to the trigger start or end edge. */\n @Prop() popupAlign: SelectPopupAlign = 'start';\n /** Shared inactive treatment; removes interaction and form submission. */\n @Prop() isInactive: boolean = false;\n /** Replace the prefix with a loader and disable option interaction. */\n @Prop() isLoading: boolean = false;\n /** Show the selected interaction fill when a valid value exists. */\n @Prop() activeFill: boolean = true;\n /** Show the surface-aware inset border, including focused and invalid strokes. */\n @Prop() hasBorder: boolean = true;\n /** Optional trigger prefix icon name. */\n @Prop() icon: string | undefined;\n /** Trailing choice indicator. Use up-down for compact value steppers such as page size. */\n @Prop() indicator: SelectIndicator = 'down';\n /** Show the clear footer action when a value exists. */\n @Prop() allowClear: boolean = true;\n /** Localized clear action label. */\n @Prop() clearLabel: string = 'Clear';\n /** Localized noun displayed after the selected count in multiple mode. */\n @Prop() selectedLabel: string = 'selected';\n /** Optional text action shown in the popup footer instead of the clear action. */\n @Prop() footerActionLabel: string | undefined;\n /** Show immediate local filtering over option labels, subtext, and section headings. */\n @Prop() searchable: boolean = false;\n /** Localized search-field placeholder and accessible name. */\n @Prop() searchPlaceholder: string = 'Search';\n /** Localized empty-filter result text. */\n @Prop() noResultsText: string = 'No results found';\n /** Accessible loading status label. */\n @Prop() loadingLabel: string = 'Loading';\n /** Actual parent surface context; omit on primary and secondary surfaces. */\n @Prop() background: SelectBackground | undefined;\n /** Show invalid visual state. */\n @Prop() error: boolean = false;\n /** Error text rendered below the trigger when error is true. */\n @Prop() errorMessage: string | undefined;\n /** ID applied to the internal combobox trigger for external labels. */\n @Prop() inputId: string | undefined;\n /** Direct accessible name when no external label is available. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** IDs of elements that label the combobox. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Additional IDs that describe the combobox. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n /** Emitted after selection or clearing with the next scalar or array value. */\n @Event() dsChange!: EventEmitter<SelectValue>;\n /** Emitted after the footer clear action. */\n @Event() dsClear!: EventEmitter<void>;\n /** Emitted whenever popup visibility changes. */\n @Event() dsOpenChange!: EventEmitter<boolean>;\n /** Emitted when the optional popup footer action is activated. */\n @Event() dsFooterAction!: EventEmitter<void>;\n /** Emitted when an option's contextual ellipsis action is activated. */\n @Event() dsOptionAction!: EventEmitter<SelectOptionActionDetail>;\n /** Emitted when an option's supporting text action is activated. */\n @Event() dsOptionSubtextAction!: EventEmitter<SelectOptionSubtextActionDetail>;\n\n @State() private activeIndex = -1;\n @State() private searchTerm = '';\n @State() private formDisabled = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private focusRingVisible = false;\n @State() private compactFooterSummary = false;\n\n private readonly generatedId = `ds-select-${++selectId}`;\n private readonly listboxId = `${this.generatedId}-listbox`;\n private readonly errorId = `${this.generatedId}-error`;\n private initialValue: SelectValue = '';\n private readonly controller = this.createController();\n private footerContentElement?: HTMLDivElement;\n private footerSummaryMeasureElement?: HTMLElement;\n private footerClearElement?: HTMLButtonElement;\n private footerResizeObserver?: ResizeObserver;\n private observedFooterContentElement?: HTMLDivElement;\n private footerMeasurementFrame?: number;\n\n private createController() {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- adapter getters preserve reactive component ownership without inheritance.\n const owner = this;\n return new SelectController<SelectOption>({\n get host() {\n return owner.el;\n },\n generatedId: owner.generatedId,\n get options() {\n return owner.visibleOptions;\n },\n get searchable() {\n return owner.searchable;\n },\n get isLoading() {\n return owner.isLoading;\n },\n get isDisabled() {\n return owner.isDisabled;\n },\n get preferredIndex() {\n if (owner.multiple) return -1;\n return owner.visibleOptions.findIndex(\n option => option.value === owner.scalarValue && !option.isInactive\n );\n },\n get popupAlign() {\n return owner.popupAlign;\n },\n get open() {\n return owner.open;\n },\n set open(value) {\n owner.open = value;\n },\n get activeIndex() {\n return owner.activeIndex;\n },\n set activeIndex(value) {\n owner.activeIndex = value;\n },\n get searchTerm() {\n return owner.searchTerm;\n },\n set searchTerm(value) {\n owner.searchTerm = value;\n },\n get focusRingVisible() {\n return owner.focusRingVisible;\n },\n set focusRingVisible(value) {\n owner.focusRingVisible = value;\n },\n get position() {\n return owner.position;\n },\n set position(value) {\n owner.position = value;\n },\n get positionReady() {\n return owner.positionReady;\n },\n set positionReady(value) {\n owner.positionReady = value;\n },\n selectOption: option => owner.selectOption(option),\n });\n }\n\n componentWillLoad() {\n this.value = this.normalizeValueForMode(this.value);\n this.initialValue = Array.isArray(this.value) ? [...this.value] : this.value;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.controller.connect();\n this.observeFooterContent();\n }\n\n componentDidRender() {\n this.observeFooterContent();\n this.scheduleFooterSummaryMeasurement();\n }\n\n disconnectedCallback() {\n this.controller.disconnect();\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n if (this.footerMeasurementFrame !== undefined) {\n cancelAnimationFrame(this.footerMeasurementFrame);\n }\n }\n\n @Watch('value')\n @Watch('multiple')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('name')\n @Watch('options')\n @Watch('sections')\n syncFormValue() {\n if (\n (this.multiple && !Array.isArray(this.value)) ||\n (!this.multiple && Array.isArray(this.value))\n ) {\n this.value = this.normalizeValueForMode(this.value);\n }\n const inactive = this.isDisabled;\n if (this.multiple) {\n const values = this.resolvedValues;\n setRepeatedFormControlValue(this.internals, this.name, values, { inactive });\n } else {\n const value = this.hasSelection ? this.scalarValue : '';\n setFormControlValue(this.internals, value, { inactive });\n }\n const missing = this.required && !inactive && !this.hasSelection;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n this.controller.optionsChanged();\n }\n\n @Watch('isLoading')\n onLoadingChange() {\n this.controller.loadingChanged();\n }\n\n @Watch('popupAlign')\n onPopupAlignChange() {\n this.controller.positionChanged();\n }\n\n @Watch('open')\n onOpenChange(open: boolean) {\n this.dsOpenChange.emit(open);\n this.controller.openChanged(open);\n }\n\n @Watch('searchTerm')\n onSearchTermChange() {\n this.controller.searchChanged();\n }\n\n @Watch('activeIndex')\n onActiveIndexChange() {\n this.controller.activeIndexChanged();\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.normalizeValueForMode(this.initialValue);\n this.closePopup();\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (!this.multiple) {\n this.value = restoreStringFormState(state);\n return;\n }\n this.value = restoreStringArrayFormState(state);\n }\n\n @Method()\n async setFocus() {\n this.controller.setFocus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private normalizeValueForMode(value: SelectValue): SelectValue {\n if (this.multiple) {\n return Array.isArray(value) ? [...value] : value ? [value] : [];\n }\n return Array.isArray(value) ? (value[0] ?? '') : value;\n }\n\n private get allSections(): SelectSection[] {\n return resolveChoiceSections(this.options, this.sections);\n }\n\n private get allOptions(): SelectOption[] {\n return flattenChoiceSections(this.allSections);\n }\n\n private get visibleSections(): SelectSection[] {\n return this.searchable\n ? filterChoiceSections(this.allSections, this.searchTerm)\n : this.allSections;\n }\n\n private get visibleOptions(): SelectOption[] {\n return flattenChoiceSections(this.visibleSections);\n }\n\n private get scalarValue(): string {\n return typeof this.value === 'string' ? this.value : '';\n }\n\n private get resolvedValues(): string[] {\n if (!this.multiple || !Array.isArray(this.value)) return [];\n const valid = new Set(this.allOptions.map(option => option.value));\n return [...new Set(this.value.filter(value => valid.has(value)))];\n }\n\n private get selectedOption(): SelectOption | undefined {\n if (this.multiple) return undefined;\n return this.allOptions.find(option => option.value === this.scalarValue);\n }\n\n private get hasSelection(): boolean {\n return this.multiple ? this.resolvedValues.length > 0 : Boolean(this.selectedOption);\n }\n\n private get activeOptionId(): string | undefined {\n return this.controller.activeOptionId;\n }\n\n private get popupRole(): 'listbox' | 'grid' {\n return this.allOptions.some(option => Boolean(option.action)) ? 'grid' : 'listbox';\n }\n\n private observeFooterContent() {\n const content =\n this.open && this.multiple && this.hasSelection ? this.footerContentElement : undefined;\n if (!content?.isConnected) {\n this.footerResizeObserver?.disconnect();\n this.observedFooterContentElement = undefined;\n return;\n }\n if (this.observedFooterContentElement === content) return;\n\n this.footerResizeObserver?.disconnect();\n if (typeof ResizeObserver !== 'undefined') {\n this.footerResizeObserver = new ResizeObserver(() => this.scheduleFooterSummaryMeasurement());\n this.footerResizeObserver.observe(content);\n }\n this.observedFooterContentElement = content;\n }\n\n private scheduleFooterSummaryMeasurement() {\n if (\n !this.open ||\n !this.multiple ||\n !this.hasSelection ||\n this.footerMeasurementFrame !== undefined\n ) {\n return;\n }\n this.footerMeasurementFrame = requestAnimationFrame(() => {\n this.footerMeasurementFrame = undefined;\n this.measureFooterSummary();\n });\n }\n\n private measureFooterSummary() {\n const content = this.footerContentElement;\n const summary = this.footerSummaryMeasureElement;\n const clear = this.footerClearElement;\n if (!content?.isConnected || !summary?.isConnected || !clear?.isConnected) return;\n\n const styles = getComputedStyle(content);\n const innerWidth =\n content.clientWidth -\n Number.parseFloat(styles.paddingInlineStart || '0') -\n Number.parseFloat(styles.paddingInlineEnd || '0');\n const requiredWidth =\n summary.getBoundingClientRect().width +\n clear.getBoundingClientRect().width +\n Number.parseFloat(styles.columnGap || '0');\n const nextCompact = requiredWidth > innerWidth + 0.5;\n if (nextCompact !== this.compactFooterSummary) {\n this.compactFooterSummary = nextCompact;\n }\n }\n\n private closePopup(restoreFocus = false) {\n this.controller.closePopup(restoreFocus);\n }\n\n private openPopup(focusVisible: boolean, edge?: 'first' | 'last') {\n this.controller.openPopup(focusVisible, edge);\n }\n\n private selectOption(option: SelectOption) {\n if (option.isInactive || this.isLoading) return;\n if (this.multiple) {\n const current = this.resolvedValues;\n const next = current.includes(option.value)\n ? current.filter(value => value !== option.value)\n : [...current, option.value];\n this.value = next;\n this.dsChange.emit([...next]);\n return;\n }\n this.value = option.value;\n this.dsChange.emit(option.value);\n this.controller.closePopup(true);\n }\n\n private clearSelection = (event: Event) => {\n event.preventDefault();\n event.stopPropagation();\n this.value = this.multiple ? [] : '';\n this.dsChange.emit(this.multiple ? [] : '');\n this.dsClear.emit();\n if (!this.multiple) {\n const enabled = enabledChoiceIndexes(this.visibleOptions);\n this.activeIndex = enabled[0] ?? -1;\n }\n this.controller.focusSearchOrTrigger();\n };\n\n private activateFooterAction = (event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsFooterAction.emit();\n this.closePopup();\n };\n\n private readonly handleListKeyDown = this.controller.handleListKeyDown;\n private readonly handleTriggerKeyDown = this.controller.handleTriggerKeyDown;\n\n private renderOption(\n option: SelectOption,\n index: number,\n usesIcons: boolean,\n usesSubtext: boolean\n ) {\n const selected = this.multiple\n ? this.resolvedValues.includes(option.value)\n : option.value === this.scalarValue;\n const active = index === this.activeIndex;\n const actionId = `${this.generatedId}-option-action-${index}`;\n return (\n <ChoiceOptionRow\n size={this.size}\n id={`${this.generatedId}-option-${index}`}\n option={option}\n selected={selected}\n active={active}\n focusRingVisible={this.focusRingVisible}\n usesSubtext={usesSubtext}\n actionOpen={option.action?.expanded}\n popupRole={this.popupRole}\n leading={\n this.multiple ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-checkbox\n class=\"select-option__checkbox\"\n label=\"\"\n size={this.size}\n checked={selected}\n presentation\n />\n </span>\n ) : usesIcons && option.icon ? (\n <span\n class=\"ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={option.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : undefined\n }\n supporting={\n option.subtextActions?.length ? (\n <span class=\"ds-choice-item__subtext ds-control-label-box select-option__subtext-actions\">\n {option.subtextActions.map((action, actionIndex) => [\n actionIndex > 0 ? (\n <ds-text\n class=\"select-option__subtext-action-separator\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ) : null,\n <button\n type=\"button\"\n class={{\n 'select-option__subtext-action': true,\n 'select-option__subtext-action--negative': action.tone === 'negative',\n 'ds-text-action': true,\n 'ds-focus-ring': true,\n }}\n onKeyDown={(event: KeyboardEvent) => event.stopPropagation()}\n onClick={(event: MouseEvent) => {\n event.preventDefault();\n event.stopPropagation();\n this.dsOptionSubtextAction.emit({\n value: option.value,\n actionValue: action.value,\n originalEvent: event,\n });\n }}\n >\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"inherit\"\n >\n {action.label}\n </ds-text>\n </button>,\n ])}\n </span>\n ) : undefined\n }\n action={\n option.action ? (\n <ds-tooltip label={option.action.label} side=\"left\" size=\"sm\">\n <ds-button-unfilled\n id={actionId}\n variant=\"icon\"\n size={OPTION_ACTION_SIZE[this.size]}\n icon=\"Ellipses\"\n rounded\n hasBorder={false}\n activeFill={false}\n ariaLabel={option.action.label}\n haspopup=\"menu\"\n controls={option.action.controls}\n expanded={option.action.expanded}\n onKeyDown={event => {\n event.stopPropagation();\n if (event.key === 'Escape' || event.key === 'ArrowLeft') {\n event.preventDefault();\n this.controller.focusSearchOrTrigger();\n }\n }}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsOptionAction.emit({\n value: option.value,\n anchorId: actionId,\n originalEvent: event.detail,\n });\n }}\n />\n </ds-tooltip>\n ) : undefined\n }\n onHover={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onSelect={() => this.selectOption(option)}\n />\n );\n }\n\n render() {\n const inactive = this.isDisabled;\n const showPlaceholder = !this.hasSelection;\n const count = this.resolvedValues.length;\n const label = this.multiple\n ? `${this.placeholder}${count > 0 ? ` · ${count}` : ''}`\n : showPlaceholder\n ? this.placeholder\n : (this.triggerLabel ?? this.selectedOption?.label);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n const usesOptionIcons = !this.multiple && choiceListUsesIcons(this.allOptions);\n const usesOptionSubtext = choiceListUsesSubtext(this.allOptions);\n const showError = this.error && Boolean(this.errorMessage);\n const describedBy =\n [this.ariaDescribedby, showError ? this.errorId : undefined].filter(Boolean).join(' ') ||\n undefined;\n const popupStyle = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n let flatIndex = 0;\n\n return (\n <Host\n class={{\n 'select-host': true,\n 'ds-select-trigger-host': true,\n 'select-host--multiple': this.multiple,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n [`select-host--background-${this.background}`]: !!this.background,\n [`ds-select-trigger-host--background-${this.background}`]: !!this.background,\n }}\n >\n <button\n ref={element => {\n this.controller.setTriggerElement((element as HTMLButtonElement) ?? null);\n }}\n id={this.inputId ?? this.generatedId}\n type=\"button\"\n class={{\n trigger: true,\n 'ds-control-frame': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': !inactive && this.activeFill && this.hasSelection,\n 'trigger--expanded': !inactive && this.open,\n 'trigger--bordered': this.hasBorder,\n 'trigger--placeholder': showPlaceholder && !this.multiple,\n 'trigger--has-value': this.hasSelection,\n 'trigger--label-placeholder':\n !this.multiple && Boolean(this.triggerLabel) && this.triggerLabelPlaceholder,\n 'wrapper--error': this.hasBorder && this.error,\n [`ds-control--${this.size}`]: true,\n ...choiceBackgroundClassMap(this.background),\n }}\n disabled={inactive}\n role=\"combobox\"\n aria-haspopup={this.popupRole}\n aria-expanded={String(this.open)}\n aria-controls={this.open ? this.listboxId : undefined}\n aria-activedescendant={this.open && !this.searchable ? this.activeOptionId : undefined}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n aria-required={this.required || undefined}\n aria-busy={this.isLoading ? 'true' : undefined}\n onClick={() => (this.open ? this.closePopup() : this.openPopup(false))}\n onKeyDown={event => {\n const action = this.visibleOptions[this.activeIndex]?.action;\n if (\n this.open &&\n action &&\n (event.key === 'ArrowRight' || (event.key === 'F10' && event.shiftKey))\n ) {\n event.preventDefault();\n document\n .getElementById(`${this.generatedId}-option-action-${this.activeIndex}`)\n ?.querySelector<HTMLElement>('button')\n ?.focus();\n return;\n }\n this.handleTriggerKeyDown(event);\n }}\n >\n {(this.icon || this.isLoading) && (\n <span\n class=\"trigger__prefix ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n {this.isLoading ? (\n <ds-loader size={iconSize} color=\"inherit\" />\n ) : (\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n )}\n </span>\n )}\n <span\n class={{\n 'trigger__label-box': true,\n 'ds-interaction-fill__content': true,\n }}\n >\n <span\n class={{\n 'trigger__label-content': true,\n 'ds-control-label-box': true,\n 'ds-control-label-dot': this.dot,\n 'trigger__label-content--dot': this.dot,\n }}\n >\n <ds-text\n class=\"trigger__label\"\n as=\"span\"\n variant={textVariant}\n color=\"inherit\"\n lineTruncation={1}\n >\n {label}\n </ds-text>\n {this.dot && (\n <ds-badge\n class=\"trigger__dot ds-control-label-dot__badge\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </span>\n <span\n class=\"trigger__chevron ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon\n name={this.indicator === 'up-down' ? 'ChevronUpDown' : 'ChevronDown'}\n size={iconSize}\n color=\"inherit\"\n />\n </span>\n </button>\n\n {this.open && (\n <div\n popover=\"manual\"\n ref={element => {\n this.controller.setPopupElement((element as HTMLDivElement) ?? null);\n }}\n class=\"select-popup ds-choice-popup\"\n style={popupStyle}\n >\n {this.searchable && (\n <ChoiceSearch\n size={this.size}\n value={this.searchTerm}\n placeholder={this.searchPlaceholder}\n controls={this.listboxId}\n activeDescendant={this.activeOptionId}\n inputRef={this.controller.setSearchElement}\n clearLabel={this.clearLabel}\n onValueChange={value => {\n this.searchTerm = value;\n }}\n onKeyDown={event => this.handleListKeyDown(event)}\n />\n )}\n <div\n id={this.listboxId}\n class=\"ds-choice-list\"\n role={this.popupRole}\n aria-multiselectable={this.multiple ? 'true' : undefined}\n aria-label={this.ariaLabel ?? this.placeholder}\n aria-busy={this.isLoading ? 'true' : undefined}\n >\n {this.isLoading ? (\n <div\n class=\"ds-choice-loading ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.loadingLabel}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-loader size={this.size} color=\"inherit\" />\n </span>\n ) : (\n <ds-loader size={this.size} color=\"inherit\" />\n )}\n </div>\n ) : this.visibleOptions.length === 0 ? (\n <div\n class=\"ds-choice-empty ds-empty-region\"\n role={this.popupRole === 'grid' ? 'row' : 'option'}\n aria-selected=\"false\"\n aria-disabled=\"true\"\n aria-label={this.noResultsText}\n aria-live=\"polite\"\n >\n {this.popupRole === 'grid' ? (\n <span role=\"gridcell\">\n <ds-empty-state body={this.noResultsText} />\n </span>\n ) : (\n <ds-empty-state body={this.noResultsText} />\n )}\n </div>\n ) : (\n this.visibleSections.map((section, sectionIndex) => (\n <div\n class={{\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': !!section.divider,\n 'ds-choice-section--headed-after-first':\n sectionIndex > 0 && Boolean(section.header),\n }}\n role={\n this.popupRole === 'grid' ? 'rowgroup' : section.header ? 'group' : undefined\n }\n aria-label={section.header}\n >\n {section.header && this.popupRole === 'grid' ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n role=\"row\"\n >\n <span class=\"ds-choice-section__header-label\" role=\"gridcell\">\n {section.header}\n </span>\n </ds-text>\n ) : section.header ? (\n <ds-text\n class={`ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\n ) : null}\n {section.options.map(option =>\n this.renderOption(option, flatIndex++, usesOptionIcons, usesOptionSubtext)\n )}\n </div>\n ))\n )}\n </div>\n {this.footerActionLabel && !this.isLoading ? (\n <ChoiceActionFooter\n size={this.size}\n label={this.footerActionLabel}\n onAction={this.activateFooterAction}\n />\n ) : this.allowClear && this.hasSelection && !this.isLoading ? (\n <ChoiceFooter\n size={this.size}\n summary={this.multiple ? `${count} ${this.selectedLabel}` : undefined}\n compactSummary={this.multiple ? String(count) : undefined}\n useCompactSummary={this.compactFooterSummary}\n contentRef={element => {\n this.footerContentElement = element ?? undefined;\n }}\n summaryMeasureRef={element => {\n this.footerSummaryMeasureElement = element ?? undefined;\n }}\n clearRef={element => {\n this.footerClearElement = element ?? undefined;\n }}\n clearLabel={this.clearLabel}\n onClear={this.clearSelection}\n />\n ) : null}\n </div>\n )}\n\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
|
|
934
934
|
"type": "registry:ui"
|
|
935
935
|
}
|
|
936
936
|
]
|