@ds-mo/ui 9.0.0 → 9.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -0
- package/dist/.build-stamp +1 -1
- package/dist/agent-patterns.json +233 -0
- package/dist/agent.json +155 -19
- package/dist/angular/ds-menu.d.ts +6 -1
- package/dist/angular/ds-menu.js +7 -4
- package/dist/angular/ds-swatch-picker.d.ts +15 -0
- package/dist/angular/ds-swatch-picker.js +42 -0
- 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-app-shell.js +1 -1
- package/dist/components/ds-bar-nav.js +1 -1
- package/dist/components/ds-bar-nav.js.map +1 -1
- package/dist/components/ds-menu.js +1 -1
- package/dist/components/ds-panel-nav.js +1 -1
- package/dist/components/ds-panel-nav.js.map +1 -1
- package/dist/components/ds-select-multi.js +1 -1
- package/dist/components/ds-select-multi.js.map +1 -1
- package/dist/components/ds-select.js +1 -1
- package/dist/components/ds-select.js.map +1 -1
- package/dist/components/ds-shell-gradient-picker.js +1 -1
- package/dist/components/ds-shell-gradient-picker.js.map +1 -1
- package/dist/components/ds-shell-gradient-swatch.js +1 -1
- package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
- package/dist/components/ds-slider.js +1 -1
- package/dist/components/ds-slider.js.map +1 -1
- package/dist/components/ds-swatch-picker.d.ts +11 -0
- package/dist/components/ds-swatch-picker.js +2 -0
- package/dist/components/ds-swatch-picker.js.map +1 -0
- package/dist/components/ds-tooltip-data-viz.js +1 -1
- package/dist/components/p-D1HFMTdC.js +2 -0
- package/dist/components/p-D1HFMTdC.js.map +1 -0
- package/dist/components/p-HBIxvS5C.js +2 -0
- package/dist/components/p-HBIxvS5C.js.map +1 -0
- package/dist/components/p-l6PmnW_P.js +2 -0
- package/dist/components/p-l6PmnW_P.js.map +1 -0
- package/dist/components/p-tQ1QHeOE.js +2 -0
- package/dist/components/p-tQ1QHeOE.js.map +1 -0
- package/dist/docs/components.json +268 -30
- package/dist/lib/components/Menu/menu-placement.d.ts +6 -3
- package/dist/lib/shell/index.d.ts +2 -2
- package/dist/lib/shell/index.js +40 -39
- package/dist/lib/shell/index.js.map +3 -3
- package/dist/lib/shell/shell-gradient-presets.d.ts +15 -0
- package/dist/lib/utils/choice-popup-alignment.d.ts +20 -0
- package/dist/lib/utils/index.d.ts +2 -0
- package/dist/lib/utils/index.js +16 -0
- package/dist/lib/utils/index.js.map +3 -3
- package/dist/mcp/cli.js +31409 -0
- package/dist/mcp-data/registry/app-shell.json +210 -0
- package/dist/mcp-data/registry/badge.json +289 -0
- package/dist/mcp-data/registry/banner.json +206 -0
- package/dist/mcp-data/registry/bar-nav.json +341 -0
- package/dist/mcp-data/registry/button-filled.json +386 -0
- package/dist/mcp-data/registry/button-unfilled.json +537 -0
- package/dist/mcp-data/registry/card-data-viz-donut.json +143 -0
- package/dist/mcp-data/registry/card-setting.json +194 -0
- package/dist/mcp-data/registry/card-shell-data-viz.json +173 -0
- package/dist/mcp-data/registry/card.json +126 -0
- package/dist/mcp-data/registry/chart-bar.json +121 -0
- package/dist/mcp-data/registry/chart-donut.json +257 -0
- package/dist/mcp-data/registry/chart-legend.json +209 -0
- package/dist/mcp-data/registry/chart-line.json +153 -0
- package/dist/mcp-data/registry/checkbox.json +361 -0
- package/dist/mcp-data/registry/chip.json +276 -0
- package/dist/mcp-data/registry/divider.json +225 -0
- package/dist/mcp-data/registry/empty-state.json +197 -0
- package/dist/mcp-data/registry/field.json +222 -0
- package/dist/mcp-data/registry/icon.json +242 -0
- package/dist/mcp-data/registry/input.json +597 -0
- package/dist/mcp-data/registry/loader.json +192 -0
- package/dist/mcp-data/registry/menu.json +459 -0
- package/dist/mcp-data/registry/modal.json +150 -0
- package/dist/mcp-data/registry/panel-nav.json +622 -0
- package/dist/mcp-data/registry/panel-sub-nav.json +249 -0
- package/dist/mcp-data/registry/panel-tools.json +374 -0
- package/dist/mcp-data/registry/radio.json +374 -0
- package/dist/mcp-data/registry/registry.json +5389 -0
- package/dist/mcp-data/registry/select-multi.json +755 -0
- package/dist/mcp-data/registry/select.json +740 -0
- package/dist/mcp-data/registry/shell-gradient-picker.json +195 -0
- package/dist/mcp-data/registry/shell-gradient-swatch.json +215 -0
- package/dist/mcp-data/registry/skeleton.json +282 -0
- package/dist/mcp-data/registry/slider.json +657 -0
- package/dist/mcp-data/registry/swatch-picker.json +231 -0
- package/dist/mcp-data/registry/switch.json +361 -0
- package/dist/mcp-data/registry/tab-group.json +255 -0
- package/dist/mcp-data/registry/tag.json +352 -0
- package/dist/mcp-data/registry/text.json +371 -0
- package/dist/mcp-data/registry/tooltip-data-viz.json +158 -0
- package/dist/mcp-data/registry/tooltip.json +311 -0
- package/dist/react/components.d.ts +1 -0
- package/dist/react/components.js +1 -0
- package/dist/react/ds-menu.d.ts +1 -0
- package/dist/react/ds-menu.js +2 -1
- package/dist/react/ds-swatch-picker.d.ts +12 -0
- package/dist/react/ds-swatch-picker.js +12 -0
- package/dist/types/components/Menu/Menu.d.ts +7 -1
- package/dist/types/components/Menu/menu-placement.d.ts +6 -3
- package/dist/types/components/Menu/menu-types.d.ts +14 -1
- package/dist/types/components/ShellGradientPicker/ShellGradientPicker.d.ts +2 -5
- package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +1 -0
- package/dist/types/components/SwatchPicker/SwatchPicker.d.ts +21 -0
- package/dist/types/components/SwatchPicker/swatch-picker-types.d.ts +26 -0
- package/dist/types/components.d.ts +117 -0
- package/dist/types/shell/index.d.ts +2 -2
- package/dist/types/shell/shell-gradient-presets.d.ts +15 -0
- package/dist/types/utils/choice-popup-alignment.d.ts +21 -0
- package/dist/types/utils/index.d.ts +2 -0
- package/package.json +6 -2
- package/dist/components/p-Bafp65bw.js +0 -2
- package/dist/components/p-Bafp65bw.js.map +0 -1
- package/dist/components/p-BdnR3jxi.js +0 -2
- package/dist/components/p-BdnR3jxi.js.map +0 -1
- package/dist/components/p-C_cu-HIT.js +0 -2
- package/dist/components/p-C_cu-HIT.js.map +0 -1
- package/dist/components/p-D0B9uDTP.js +0 -2
- package/dist/components/p-D0B9uDTP.js.map +0 -1
- package/dist/components/p-DF0yLZG_.js +0 -2
- package/dist/components/p-DF0yLZG_.js.map +0 -1
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "app-shell",
|
|
4
|
+
"title": "AppShell",
|
|
5
|
+
"description": "The single required workspace root for standard authenticated application pages, coordinating primary and secondary navigation, tools chrome, responsive composition, content geometry, shared styling, and shell behavior.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/app-shell",
|
|
11
|
+
"source": "src/wc/components/AppShell/AppShell.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "specialized",
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"summary": "The single required workspace root for standard authenticated application pages, coordinating primary and secondary navigation, tools chrome, responsive composition, content geometry, shared styling, and shell behavior.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"Building any standard authenticated product or workspace page with CompoMo.",
|
|
19
|
+
"PanelNav, BarNav, PanelTools, page content, shared chrome, responsive behavior, and shell shortcuts must be composed as one application frame.",
|
|
20
|
+
"Future application templates need the supported base-page composition boundary."
|
|
21
|
+
],
|
|
22
|
+
"avoidWhen": [
|
|
23
|
+
"Building authentication, onboarding, public, marketing, print, error, or intentionally focused full-screen experiences that require a different layout.",
|
|
24
|
+
"Another AppShell already exists in the page; applications must render exactly one and must never nest AppShells.",
|
|
25
|
+
"Building content inside an existing AppShell; render the page content rather than another shell."
|
|
26
|
+
],
|
|
27
|
+
"commonlyComposedWith": [
|
|
28
|
+
"component:ds-panel-nav",
|
|
29
|
+
"component:ds-bar-nav",
|
|
30
|
+
"component:ds-panel-tools",
|
|
31
|
+
"component:ds-swatch-picker"
|
|
32
|
+
],
|
|
33
|
+
"patterns": [
|
|
34
|
+
"pattern:application-shell"
|
|
35
|
+
],
|
|
36
|
+
"accessibility": [
|
|
37
|
+
"Keep primary destinations in PanelNav and secondary route sections in BarNav.",
|
|
38
|
+
"Ensure the default content region contains the page's single main landmark.",
|
|
39
|
+
"Keep PanelNav, BarNav, and PanelTools chrome available in the shell's supported desktop and tablet compositions.",
|
|
40
|
+
"Do not override browser keyboard chords when enabling shell shortcuts.",
|
|
41
|
+
"Require Search and Help as the minimum PanelTools actions and provide accessible labels for every tool.",
|
|
42
|
+
"Preserve focus and accessible names across navigation, collapse, overflow, and responsive presentation changes."
|
|
43
|
+
],
|
|
44
|
+
"states": [
|
|
45
|
+
"Render exactly one AppShell as the workspace root and never nest it.",
|
|
46
|
+
"PanelNav, BarNav, and PanelTools are required shell regions; PanelTools contains at least Search and Help even when no additional product tools apply.",
|
|
47
|
+
"The application router owns the current route, authorization filtering, Dashboard or Settings context, destination collections, section collections, and localized content labels.",
|
|
48
|
+
"AppShell propagates navStyle and coordinates layout, gradient alignment, transitions, metrics, shortcuts, and responsive composition.",
|
|
49
|
+
"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.",
|
|
50
|
+
"The application owns gradient preference persistence, cross-tab synchronization, and server or account precedence; AppShell owns rendering the resolved preset across authenticated routes.",
|
|
51
|
+
"Dashboard represents day-to-day operational work while Settings represents administrative and configuration work; route state selects the context.",
|
|
52
|
+
"Update PanelNav groups, BarNav tabs and heading, navStyle, basePath, and currentUrl atomically from one resolved route state.",
|
|
53
|
+
"BarNav remains present: it shows route-section tabs when available and the active PanelNav destination label as heading otherwise.",
|
|
54
|
+
"The application filters unauthorized destinations, sections, and tools before passing them into shell components."
|
|
55
|
+
],
|
|
56
|
+
"responsiveBehavior": [
|
|
57
|
+
"AppShell owns the fixed system breakpoints and resolves desktop at 1200px and wider, tablet from 768px through 1199px, and mobile below 768px.",
|
|
58
|
+
"On desktop, PanelNav defaults expanded, permits user collapse or expansion, and respects the locally persisted desktop preference.",
|
|
59
|
+
"On tablet, AppShell keeps PanelNav permanently collapsed and prevents user expansion without overwriting the desktop preference.",
|
|
60
|
+
"On mobile, AppShell remains the page root but replaces PanelNav and desktop navigation composition with a future dedicated mobile navigation design.",
|
|
61
|
+
"Keep PanelNav width, BarNav height, PanelTools chrome, and main-content origin stable except during intentional responsive or collapse transitions.",
|
|
62
|
+
"The current runtime does not yet implement the complete desktop, tablet, and mobile composition contract; consuming applications must enforce missing breakpoint behavior until AppShell provides it."
|
|
63
|
+
],
|
|
64
|
+
"frameworkCaveats": {
|
|
65
|
+
"customElements": [
|
|
66
|
+
"Assign complex navigation and tool collections as JavaScript properties on the slotted components.",
|
|
67
|
+
"Stamp route-derived nav-style before components connect when deterministic first paint is required."
|
|
68
|
+
],
|
|
69
|
+
"react": [
|
|
70
|
+
"Use generated wrappers and preserve native slot names for PanelNav, BarNav, PanelTools, and main content.",
|
|
71
|
+
"Resolve all shell inputs from one router state before rendering the composition."
|
|
72
|
+
],
|
|
73
|
+
"angular": [
|
|
74
|
+
"Use generated standalone adapters and preserve slot attributes in templates.",
|
|
75
|
+
"Bind complex inputs as properties and update navigation inputs atomically from one resolved router state.",
|
|
76
|
+
"Bind both nav-style attributes and properties when deterministic first paint is required."
|
|
77
|
+
]
|
|
78
|
+
},
|
|
79
|
+
"references": [
|
|
80
|
+
{
|
|
81
|
+
"label": "Component guide",
|
|
82
|
+
"path": "src/wc/components/AppShell/AppShell.mdx"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"label": "Executable Storybook composition",
|
|
86
|
+
"path": "src/wc/components/AppShell/AppShell.stories.ts"
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"label": "Framework integration",
|
|
90
|
+
"path": "docs/framework-integration.md"
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"label": "Application shell contract",
|
|
94
|
+
"path": "agent/patterns/application-shell/pattern.agent.json"
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
},
|
|
98
|
+
"api": {
|
|
99
|
+
"props": {
|
|
100
|
+
"gradientPreset": {
|
|
101
|
+
"type": "ShellGradientPreset",
|
|
102
|
+
"resolvedType": "\"cool\" | \"fresh\" | \"neutral\" | \"none\" | \"warm\"",
|
|
103
|
+
"attribute": "gradient-preset",
|
|
104
|
+
"default": "'neutral'",
|
|
105
|
+
"required": false,
|
|
106
|
+
"mutable": false,
|
|
107
|
+
"description": "Shell chrome wash preset. `none` renders solid chrome; the remaining\npresets use token-based washes that adapt to the active color theme."
|
|
108
|
+
},
|
|
109
|
+
"navStyle": {
|
|
110
|
+
"type": "NavChromeStyle",
|
|
111
|
+
"resolvedType": "\"dashboard\" | \"settings\"",
|
|
112
|
+
"attribute": "nav-style",
|
|
113
|
+
"default": "'dashboard'",
|
|
114
|
+
"required": false,
|
|
115
|
+
"mutable": false,
|
|
116
|
+
"description": "Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`."
|
|
117
|
+
},
|
|
118
|
+
"shortcutsEnabled": {
|
|
119
|
+
"type": "boolean",
|
|
120
|
+
"resolvedType": "boolean",
|
|
121
|
+
"attribute": "shortcuts-enabled",
|
|
122
|
+
"default": "true",
|
|
123
|
+
"required": false,
|
|
124
|
+
"mutable": false,
|
|
125
|
+
"description": "When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K/A/S/M/N/? toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native."
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"events": [],
|
|
129
|
+
"methods": [],
|
|
130
|
+
"slots": []
|
|
131
|
+
},
|
|
132
|
+
"props": {
|
|
133
|
+
"gradientPreset": {
|
|
134
|
+
"type": "ShellGradientPreset",
|
|
135
|
+
"resolvedType": "\"cool\" | \"fresh\" | \"neutral\" | \"none\" | \"warm\"",
|
|
136
|
+
"attribute": "gradient-preset",
|
|
137
|
+
"default": "'neutral'",
|
|
138
|
+
"required": false,
|
|
139
|
+
"mutable": false,
|
|
140
|
+
"description": "Shell chrome wash preset. `none` renders solid chrome; the remaining\npresets use token-based washes that adapt to the active color theme."
|
|
141
|
+
},
|
|
142
|
+
"navStyle": {
|
|
143
|
+
"type": "NavChromeStyle",
|
|
144
|
+
"resolvedType": "\"dashboard\" | \"settings\"",
|
|
145
|
+
"attribute": "nav-style",
|
|
146
|
+
"default": "'dashboard'",
|
|
147
|
+
"required": false,
|
|
148
|
+
"mutable": false,
|
|
149
|
+
"description": "Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`."
|
|
150
|
+
},
|
|
151
|
+
"shortcutsEnabled": {
|
|
152
|
+
"type": "boolean",
|
|
153
|
+
"resolvedType": "boolean",
|
|
154
|
+
"attribute": "shortcuts-enabled",
|
|
155
|
+
"default": "true",
|
|
156
|
+
"required": false,
|
|
157
|
+
"mutable": false,
|
|
158
|
+
"description": "When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K/A/S/M/N/? toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native."
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
"events": [],
|
|
162
|
+
"methods": [],
|
|
163
|
+
"slots": [],
|
|
164
|
+
"exports": {
|
|
165
|
+
"customElement": "ds-app-shell",
|
|
166
|
+
"react": "DsAppShell",
|
|
167
|
+
"angular": "DsAppShell"
|
|
168
|
+
},
|
|
169
|
+
"consumption": {
|
|
170
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
171
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
172
|
+
"customElements": {
|
|
173
|
+
"import": "import '@ds-mo/ui/dist/components/ds-app-shell.js';",
|
|
174
|
+
"example": "<ds-app-shell></ds-app-shell>"
|
|
175
|
+
},
|
|
176
|
+
"react": {
|
|
177
|
+
"import": "import { DsAppShell } from '@ds-mo/ui/react';",
|
|
178
|
+
"example": "<DsAppShell />"
|
|
179
|
+
},
|
|
180
|
+
"angular": {
|
|
181
|
+
"import": "import { DsAppShell } from '@ds-mo/ui/angular/ds-app-shell';",
|
|
182
|
+
"example": "<ds-app-shell></ds-app-shell>"
|
|
183
|
+
},
|
|
184
|
+
"peerDependencies": {
|
|
185
|
+
"required": [
|
|
186
|
+
"@ds-mo/tokens ^3.1.1",
|
|
187
|
+
"@ds-mo/icons ^6.2.0"
|
|
188
|
+
],
|
|
189
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
"dependencies": [
|
|
194
|
+
"@ds-mo/ui",
|
|
195
|
+
"@ds-mo/tokens"
|
|
196
|
+
],
|
|
197
|
+
"registryDependencies": [],
|
|
198
|
+
"files": [
|
|
199
|
+
{
|
|
200
|
+
"path": "src/wc/components/AppShell/AppShell.css",
|
|
201
|
+
"content": ":host {\n display: block;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n --ds-shell-panel-tools-width: var(--dimension-panel-width-sm);\n}\n\n@media (min-width: 768px) and (max-width: 1199px) {\n :host {\n --ds-shell-panel-tools-width: var(--dimension-panel-width-xs);\n }\n}\n\n.app-shell {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n font-family: var(--typography-font-family, system-ui);\n}\n\n:host {\n --_shell-chrome-bg: var(--color-background-secondary);\n}\n\n.app-shell__row {\n display: flex;\n flex: 1 1 auto;\n min-height: 0;\n height: 100%;\n position: relative;\n}\n\n/*\n * One chrome layer behind the shell row. Wash uses background-attachment: fixed\n * so the pattern is locked to the viewport — panel/tools collapse only changes the\n * transparent window, not the pattern phase. JS sets wash background-size from\n * readShellViewportDimensions(), not the shell element box.\n */\n.app-shell__chrome {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 0;\n background-color: var(--_shell-chrome-bg);\n}\n\n:host(.app-shell--gradient) .app-shell__chrome::before {\n content: '';\n position: absolute;\n inset: 0;\n pointer-events: none;\n background-position: 0 0;\n background-attachment: fixed;\n z-index: 1;\n opacity: var(--ds-shell-gradient-opacity, 1);\n background-image: var(--ds-shell-gradient-image, none);\n background-size: var(--ds-shell-gradient-size, auto);\n background-repeat: no-repeat;\n}\n\n.app-shell__panel {\n flex: 0 0 auto;\n min-height: 0;\n height: 100%;\n position: relative;\n /* Above main so resize handle line can paint over the workspace edge */\n z-index: 2;\n}\n\n.app-shell__panel ::slotted(ds-panel-nav) {\n display: block;\n height: 100%;\n}\n\n.app-shell__main {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n grid-template-rows: auto minmax(0, 1fr);\n grid-template-areas:\n 'bar tools'\n 'content tools';\n position: relative;\n z-index: 1;\n}\n\n.app-shell__bar {\n grid-area: bar;\n flex: 0 0 auto;\n min-width: 0;\n}\n\n.app-shell__bar ::slotted(ds-bar-nav) {\n display: block;\n}\n\n.app-shell__content {\n grid-area: content;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n overflow: auto;\n /* Limit reflow to the page column — not on __main (breaks tools flex shrink). */\n contain: layout;\n}\n\n/* Opaque page surface — masks the fixed chrome layer in the content column. */\n.app-shell__content {\n position: relative;\n z-index: 1;\n background-color: var(--color-background-primary);\n}\n\n.app-shell__tools {\n grid-area: tools;\n flex: 0 0 auto;\n min-width: 0;\n min-height: 0;\n height: 100%;\n display: flex;\n position: relative;\n z-index: 0;\n overflow: hidden;\n}\n\n.app-shell__tools ::slotted(ds-panel-tools) {\n display: block;\n height: 100%;\n min-height: 0;\n}\n",
|
|
202
|
+
"type": "registry:ui"
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"path": "src/wc/components/AppShell/AppShell.tsx",
|
|
206
|
+
"content": "import { Component, Prop, Element, Watch, Listen, h, Host } from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport {\n isEditableShortcutTarget,\n resolveShellShortcut,\n} from '../../shell/shell-shortcuts';\nimport type { PanelToolsToolId } from '../PanelTools/panel-tools-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\n\n@Component({\n tag: 'ds-app-shell',\n styleUrl: 'AppShell.css',\n scoped: true,\n})\nexport class AppShell {\n /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /**\n * Shell chrome wash preset. `none` renders solid chrome; the remaining\n * presets use token-based washes that adapt to the active color theme.\n */\n @Prop({ attribute: 'gradient-preset', reflect: true }) gradientPreset: ShellGradientPreset =\n DEFAULT_SHELL_GRADIENT_PRESET;\n\n /** When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K/A/S/M/N/? toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native. */\n @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;\n\n @Element() el!: HTMLElement;\n\n private resizeObserver: ResizeObserver | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n componentDidLoad() {\n this.syncSlottedNavStyle();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n requestAnimationFrame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.chromeSyncCoalescer.cancel();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n private onChromeTransitionStart = (event: Event) => {\n if (readChromeTransitionSource(event) !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n if (readChromeTransitionSource(event) !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;\n const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.app-shell__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n ];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string,\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.app-shell__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return normalizeShellGradientPreset(this.gradientPreset) !== 'none';\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-panel-tools') as HTMLDsPanelToolsElement | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool(toolId);\n }\n }\n\n private syncChrome() {\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target.tagName.toLowerCase() === 'ds-bar-nav';\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const shellCls: Record<string, boolean> = {\n 'app-shell': true,\n 'app-shell--gradient': chromeActive,\n [`app-shell--${this.navStyle}`]: true,\n };\n\n return (\n <Host class={shellCls}>\n <div class=\"app-shell__row\">\n <div class=\"app-shell__chrome\" aria-hidden=\"true\" />\n <div class=\"app-shell__panel\">\n <slot name=\"panel\" />\n </div>\n <div class=\"app-shell__main\">\n <div class=\"app-shell__bar\">\n <slot name=\"bar\" />\n </div>\n <div class=\"app-shell__tools\">\n <slot name=\"tools\" />\n </div>\n <div class=\"app-shell__content\">\n <slot />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
|
|
207
|
+
"type": "registry:ui"
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
3
|
+
"name": "badge",
|
|
4
|
+
"title": "Badge",
|
|
5
|
+
"description": "Non-interactive brand counter or dot that supplements an owning control or label with unread notification information.",
|
|
6
|
+
"type": "registry:ui",
|
|
7
|
+
"meta": {
|
|
8
|
+
"library": "@ds-mo/ui",
|
|
9
|
+
"distribution": "npm",
|
|
10
|
+
"storybook": "https://zainadeel.github.io/compomo/?path=/story/badge",
|
|
11
|
+
"source": "src/wc/components/Badge/Badge.tsx",
|
|
12
|
+
"intentStatus": "complete",
|
|
13
|
+
"intent": {
|
|
14
|
+
"audience": "general",
|
|
15
|
+
"status": "stable",
|
|
16
|
+
"summary": "Non-interactive brand counter or dot that supplements an owning control or label with unread notification information.",
|
|
17
|
+
"useWhen": [
|
|
18
|
+
"An existing control, navigation item, or label needs a compact unread notification count.",
|
|
19
|
+
"An owning element needs a small visual dot indicating that supplemental notification information exists.",
|
|
20
|
+
"When the badge overlaps an icon or content, the immediate backing surface can be identified so its separation ring matches."
|
|
21
|
+
],
|
|
22
|
+
"avoidWhen": [
|
|
23
|
+
"The status must stand alone without an owning label or control.",
|
|
24
|
+
"The mark needs semantic positive, caution, negative, or neutral color; Badge intentionally uses one brand treatment.",
|
|
25
|
+
"The compact content is metadata or taxonomy; use Tag or Chip according to whether it is removable.",
|
|
26
|
+
"The badge itself would receive pointer or keyboard interaction; put interaction on the owning control."
|
|
27
|
+
],
|
|
28
|
+
"alternatives": [
|
|
29
|
+
{
|
|
30
|
+
"when": "A compact static label communicates taxonomy or a semantic status.",
|
|
31
|
+
"component": "component:ds-tag",
|
|
32
|
+
"reason": "Tag owns static metadata labels and semantic intent coloring."
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"when": "A metadata value must be individually removable.",
|
|
36
|
+
"component": "component:ds-chip",
|
|
37
|
+
"reason": "Chip owns dismissible metadata values."
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"when": "The count is primary content rather than supplemental notification chrome.",
|
|
41
|
+
"component": "component:ds-text",
|
|
42
|
+
"reason": "Text preserves the quantity as normal readable content."
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"commonlyComposedWith": [
|
|
46
|
+
"component:ds-button-unfilled",
|
|
47
|
+
"component:ds-bar-nav",
|
|
48
|
+
"component:ds-panel-nav",
|
|
49
|
+
"component:ds-menu"
|
|
50
|
+
],
|
|
51
|
+
"accessibility": [
|
|
52
|
+
"The owning control or content provides the primary accessible name; Badge contributes only supplemental unread or notification meaning.",
|
|
53
|
+
"When a dot is announced, provide contextual text that explains its meaning. Otherwise mark it aria-hidden from the owning composition.",
|
|
54
|
+
"When a counter is announced, provide contextual text such as “5 unread notifications”; a bare number is insufficient.",
|
|
55
|
+
"Avoid duplicating information already included in the owning control's accessible name.",
|
|
56
|
+
"Badge never receives focus and never owns an interaction."
|
|
57
|
+
],
|
|
58
|
+
"states": [
|
|
59
|
+
"Counter represents an unread or notification count, while dot communicates presence without quantity.",
|
|
60
|
+
"Non-positive counter values hide the badge.",
|
|
61
|
+
"Counts above the configured compact limit display the limit with a plus suffix while accessible text retains meaningful context.",
|
|
62
|
+
"Badge keeps one brand treatment; semantic severity belongs to another component or adjacent content.",
|
|
63
|
+
"Use the ring only when Badge overlaps an icon or other content. Disable it in a reserved safe-area slot.",
|
|
64
|
+
"When enabled, the ring matches the immediate backing surface for both variants. Use a direct ring override only for a component-local fill that no surface preset represents.",
|
|
65
|
+
"Enabled shell gradient rings align automatically inside active AppShell gradient chrome and may be explicitly enabled or disabled when composition requires it."
|
|
66
|
+
],
|
|
67
|
+
"responsiveBehavior": [
|
|
68
|
+
"The owning control or layout positions Badge and decides whether notification indicators remain visible, condense, or move at responsive breakpoints.",
|
|
69
|
+
"Badge does not anchor itself or automatically switch between counter and dot variants."
|
|
70
|
+
],
|
|
71
|
+
"frameworkCaveats": {
|
|
72
|
+
"customElements": [
|
|
73
|
+
"Position Badge from the owning element's layout and synchronize its contextual accessibility text with the owner."
|
|
74
|
+
],
|
|
75
|
+
"react": [
|
|
76
|
+
"Keep count, visibility, and contextual accessibility text derived from the same unread-notification state as the owning control."
|
|
77
|
+
],
|
|
78
|
+
"angular": [
|
|
79
|
+
"Keep count, visibility, and contextual accessibility text derived from the same unread-notification state as the owning control."
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
"references": [
|
|
83
|
+
{
|
|
84
|
+
"label": "Storybook examples",
|
|
85
|
+
"path": "src/wc/components/Badge/Badge.stories.ts"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"api": {
|
|
90
|
+
"props": {
|
|
91
|
+
"background": {
|
|
92
|
+
"type": "string | undefined",
|
|
93
|
+
"resolvedType": "string | undefined",
|
|
94
|
+
"attribute": "background",
|
|
95
|
+
"required": false,
|
|
96
|
+
"mutable": false,
|
|
97
|
+
"description": "Direct ring background override for component-local surfaces."
|
|
98
|
+
},
|
|
99
|
+
"count": {
|
|
100
|
+
"type": "number",
|
|
101
|
+
"resolvedType": "number",
|
|
102
|
+
"attribute": "count",
|
|
103
|
+
"default": "0",
|
|
104
|
+
"required": false,
|
|
105
|
+
"mutable": false,
|
|
106
|
+
"description": "Count shown for counter badges. Count 0 hides the badge."
|
|
107
|
+
},
|
|
108
|
+
"gradientBackground": {
|
|
109
|
+
"type": "boolean",
|
|
110
|
+
"resolvedType": "boolean",
|
|
111
|
+
"attribute": "gradient-background",
|
|
112
|
+
"default": "false",
|
|
113
|
+
"required": false,
|
|
114
|
+
"mutable": false,
|
|
115
|
+
"description": "Ring samples the shell gradient stack (base fill + wash) instead of a flat\n`box-shadow`. Auto-enabled under an AppShell with an active gradient preset;\nset `gradient-background` to opt in/out explicitly.\n\nThe attribute must NOT start with `on` — Stencil's setAccessor routes any\nunknown `on*` member down the event-listener path during attribute\nreflection, calling addEventListener with a non-listener and throwing."
|
|
116
|
+
},
|
|
117
|
+
"hasRing": {
|
|
118
|
+
"type": "boolean",
|
|
119
|
+
"resolvedType": "boolean",
|
|
120
|
+
"attribute": "has-ring",
|
|
121
|
+
"default": "true",
|
|
122
|
+
"required": false,
|
|
123
|
+
"mutable": false,
|
|
124
|
+
"description": "Show the separation ring when the badge overlaps an icon or other content."
|
|
125
|
+
},
|
|
126
|
+
"label": {
|
|
127
|
+
"type": "string | undefined",
|
|
128
|
+
"resolvedType": "string | undefined",
|
|
129
|
+
"attribute": "label",
|
|
130
|
+
"required": false,
|
|
131
|
+
"mutable": false,
|
|
132
|
+
"description": "Contextual supplemental text. Omit when the owner hides the badge from assistive technology."
|
|
133
|
+
},
|
|
134
|
+
"max": {
|
|
135
|
+
"type": "number",
|
|
136
|
+
"resolvedType": "number",
|
|
137
|
+
"attribute": "max",
|
|
138
|
+
"default": "9",
|
|
139
|
+
"required": false,
|
|
140
|
+
"mutable": false,
|
|
141
|
+
"description": "Highest count shown before compacting to \"{max}+\"."
|
|
142
|
+
},
|
|
143
|
+
"surface": {
|
|
144
|
+
"type": "BadgeSurface",
|
|
145
|
+
"resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"navigation\" | \"primary\" | \"secondary\" | \"strong\" | \"translucent\"",
|
|
146
|
+
"attribute": "surface",
|
|
147
|
+
"default": "'primary'",
|
|
148
|
+
"required": false,
|
|
149
|
+
"mutable": false,
|
|
150
|
+
"description": "Immediate backing surface matched by the ring around either variant."
|
|
151
|
+
},
|
|
152
|
+
"variant": {
|
|
153
|
+
"type": "BadgeVariant",
|
|
154
|
+
"resolvedType": "\"counter\" | \"dot\"",
|
|
155
|
+
"attribute": "variant",
|
|
156
|
+
"default": "'counter'",
|
|
157
|
+
"required": false,
|
|
158
|
+
"mutable": false,
|
|
159
|
+
"description": "Render as a compact counter or notification dot."
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"events": [],
|
|
163
|
+
"methods": [],
|
|
164
|
+
"slots": []
|
|
165
|
+
},
|
|
166
|
+
"props": {
|
|
167
|
+
"background": {
|
|
168
|
+
"type": "string | undefined",
|
|
169
|
+
"resolvedType": "string | undefined",
|
|
170
|
+
"attribute": "background",
|
|
171
|
+
"required": false,
|
|
172
|
+
"mutable": false,
|
|
173
|
+
"description": "Direct ring background override for component-local surfaces."
|
|
174
|
+
},
|
|
175
|
+
"count": {
|
|
176
|
+
"type": "number",
|
|
177
|
+
"resolvedType": "number",
|
|
178
|
+
"attribute": "count",
|
|
179
|
+
"default": "0",
|
|
180
|
+
"required": false,
|
|
181
|
+
"mutable": false,
|
|
182
|
+
"description": "Count shown for counter badges. Count 0 hides the badge."
|
|
183
|
+
},
|
|
184
|
+
"gradientBackground": {
|
|
185
|
+
"type": "boolean",
|
|
186
|
+
"resolvedType": "boolean",
|
|
187
|
+
"attribute": "gradient-background",
|
|
188
|
+
"default": "false",
|
|
189
|
+
"required": false,
|
|
190
|
+
"mutable": false,
|
|
191
|
+
"description": "Ring samples the shell gradient stack (base fill + wash) instead of a flat\n`box-shadow`. Auto-enabled under an AppShell with an active gradient preset;\nset `gradient-background` to opt in/out explicitly.\n\nThe attribute must NOT start with `on` — Stencil's setAccessor routes any\nunknown `on*` member down the event-listener path during attribute\nreflection, calling addEventListener with a non-listener and throwing."
|
|
192
|
+
},
|
|
193
|
+
"hasRing": {
|
|
194
|
+
"type": "boolean",
|
|
195
|
+
"resolvedType": "boolean",
|
|
196
|
+
"attribute": "has-ring",
|
|
197
|
+
"default": "true",
|
|
198
|
+
"required": false,
|
|
199
|
+
"mutable": false,
|
|
200
|
+
"description": "Show the separation ring when the badge overlaps an icon or other content."
|
|
201
|
+
},
|
|
202
|
+
"label": {
|
|
203
|
+
"type": "string | undefined",
|
|
204
|
+
"resolvedType": "string | undefined",
|
|
205
|
+
"attribute": "label",
|
|
206
|
+
"required": false,
|
|
207
|
+
"mutable": false,
|
|
208
|
+
"description": "Contextual supplemental text. Omit when the owner hides the badge from assistive technology."
|
|
209
|
+
},
|
|
210
|
+
"max": {
|
|
211
|
+
"type": "number",
|
|
212
|
+
"resolvedType": "number",
|
|
213
|
+
"attribute": "max",
|
|
214
|
+
"default": "9",
|
|
215
|
+
"required": false,
|
|
216
|
+
"mutable": false,
|
|
217
|
+
"description": "Highest count shown before compacting to \"{max}+\"."
|
|
218
|
+
},
|
|
219
|
+
"surface": {
|
|
220
|
+
"type": "BadgeSurface",
|
|
221
|
+
"resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"navigation\" | \"primary\" | \"secondary\" | \"strong\" | \"translucent\"",
|
|
222
|
+
"attribute": "surface",
|
|
223
|
+
"default": "'primary'",
|
|
224
|
+
"required": false,
|
|
225
|
+
"mutable": false,
|
|
226
|
+
"description": "Immediate backing surface matched by the ring around either variant."
|
|
227
|
+
},
|
|
228
|
+
"variant": {
|
|
229
|
+
"type": "BadgeVariant",
|
|
230
|
+
"resolvedType": "\"counter\" | \"dot\"",
|
|
231
|
+
"attribute": "variant",
|
|
232
|
+
"default": "'counter'",
|
|
233
|
+
"required": false,
|
|
234
|
+
"mutable": false,
|
|
235
|
+
"description": "Render as a compact counter or notification dot."
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
"events": [],
|
|
239
|
+
"methods": [],
|
|
240
|
+
"slots": [],
|
|
241
|
+
"exports": {
|
|
242
|
+
"customElement": "ds-badge",
|
|
243
|
+
"react": "DsBadge",
|
|
244
|
+
"angular": "DsBadge"
|
|
245
|
+
},
|
|
246
|
+
"consumption": {
|
|
247
|
+
"install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
|
|
248
|
+
"cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
|
|
249
|
+
"customElements": {
|
|
250
|
+
"import": "import '@ds-mo/ui/dist/components/ds-badge.js';",
|
|
251
|
+
"example": "<ds-badge></ds-badge>"
|
|
252
|
+
},
|
|
253
|
+
"react": {
|
|
254
|
+
"import": "import { DsBadge } from '@ds-mo/ui/react';",
|
|
255
|
+
"example": "<DsBadge />"
|
|
256
|
+
},
|
|
257
|
+
"angular": {
|
|
258
|
+
"import": "import { DsBadge } from '@ds-mo/ui/angular/ds-badge';",
|
|
259
|
+
"example": "<ds-badge></ds-badge>"
|
|
260
|
+
},
|
|
261
|
+
"peerDependencies": {
|
|
262
|
+
"required": [
|
|
263
|
+
"@ds-mo/tokens ^3.1.1",
|
|
264
|
+
"@ds-mo/icons ^6.2.0"
|
|
265
|
+
],
|
|
266
|
+
"frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
"dependencies": [
|
|
271
|
+
"@ds-mo/ui",
|
|
272
|
+
"@ds-mo/tokens"
|
|
273
|
+
],
|
|
274
|
+
"registryDependencies": [
|
|
275
|
+
"text"
|
|
276
|
+
],
|
|
277
|
+
"files": [
|
|
278
|
+
{
|
|
279
|
+
"path": "src/wc/components/Badge/Badge.css",
|
|
280
|
+
"content": ":host {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n vertical-align: middle;\n\n --_badge-bg: var(--color-background-bold-brand);\n --_badge-fg: var(--color-foreground-on-bold-background-primary);\n --_badge-ring: var(--color-background-secondary);\n --_badge-ring-width: var(--dimension-stroke-width-018);\n}\n\n.badge__mark {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n box-sizing: border-box;\n color: var(--_badge-fg);\n background: var(--_badge-bg);\n box-shadow: 0 0 0 var(--_badge-ring-width) var(--_badge-ring);\n white-space: nowrap;\n}\n\n:host(.badge--no-ring) {\n --_badge-ring-width: 0;\n}\n\n.badge__a11y {\n position: absolute;\n /* stylelint-disable declaration-property-value-disallowed-list -- classic visually-hidden clip pattern */\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n /* stylelint-enable declaration-property-value-disallowed-list */\n}\n\n:host(.badge--dot) .badge__mark {\n width: var(--dimension-size-075);\n height: var(--dimension-size-075);\n border-radius: var(--dimension-radius-half);\n}\n\n:host(.badge--counter) .badge__mark {\n width: auto;\n min-width: var(--dimension-size-150);\n height: var(--dimension-size-150);\n padding: 0 var(--dimension-space-025);\n border-radius: var(--dimension-radius-half);\n}\n\n/* Shell gradient ring — base fill + wash donut (option 3). */\n:host(.badge--on-gradient-background) .badge__mark {\n position: relative;\n z-index: 0;\n box-shadow: none;\n isolation: isolate;\n}\n\n:host(.badge--on-gradient-background) .badge__mark::before,\n:host(.badge--on-gradient-background) .badge__mark::after {\n content: '';\n position: absolute;\n inset: calc(-1 * var(--_badge-ring-width));\n border-radius: inherit;\n box-sizing: border-box;\n padding: var(--_badge-ring-width);\n pointer-events: none;\n /* stylelint-disable color-no-hex -- mask luminance stops for ring cutout, not theme colors */\n -webkit-mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n -webkit-mask-composite: xor;\n mask:\n linear-gradient(#fff 0 0) content-box,\n linear-gradient(#fff 0 0);\n /* stylelint-enable color-no-hex */\n mask-composite: exclude;\n}\n\n:host(.badge--on-gradient-background) .badge__mark::before {\n z-index: -2;\n background-color: var(--_badge-ring);\n}\n\n:host(.badge--on-gradient-background) .badge__mark::after {\n z-index: -1;\n background-image: var(--ds-shell-gradient-image, none);\n background-size: var(--ds-shell-gradient-size, auto);\n background-position: 0 0;\n background-repeat: no-repeat;\n background-attachment: fixed;\n opacity: var(--ds-shell-gradient-opacity, 1);\n}\n",
|
|
281
|
+
"type": "registry:ui"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"path": "src/wc/components/Badge/Badge.tsx",
|
|
285
|
+
"content": "import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';\nimport {\n BADGE_GRADIENT_POSITION_VAR,\n isShellGradientActive,\n} from '../../shell/badge-gradient-ring';\n\nexport type BadgeVariant = 'counter' | 'dot';\nexport type BadgeSurface =\n | 'primary'\n | 'secondary'\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'navigation'\n | 'always-dark';\n\nconst SURFACE_RING: Record<BadgeSurface, string> = {\n primary: 'var(--color-background-primary)',\n secondary: 'var(--color-background-secondary)',\n faint: 'var(--color-background-faint-neutral)',\n medium: 'var(--color-background-medium-neutral)',\n bold: 'var(--color-background-bold-neutral)',\n strong: 'var(--color-background-strong-neutral)',\n translucent: 'var(--color-translucent-translucent)',\n inverted: 'var(--color-inverted-background)',\n media: 'var(--color-media-background)',\n navigation: 'var(--color-navigation-background)',\n 'always-dark': 'var(--color-always-dark-background)',\n};\n\n@Component({\n tag: 'ds-badge',\n styleUrl: 'Badge.css',\n scoped: true,\n})\nexport class Badge {\n @Element() el!: HTMLElement;\n\n /** Render as a compact counter or notification dot. */\n @Prop() variant: BadgeVariant = 'counter';\n\n /** Count shown for counter badges. Count 0 hides the badge. */\n @Prop() count: number = 0;\n\n /** Highest count shown before compacting to \"{max}+\". */\n @Prop() max: number = 9;\n\n /** Immediate backing surface matched by the ring around either variant. */\n @Prop() surface: BadgeSurface = 'primary';\n\n /** Direct ring background override for component-local surfaces. */\n @Prop() background: string | undefined;\n\n /** Show the separation ring when the badge overlaps an icon or other content. */\n @Prop() hasRing: boolean = true;\n\n /**\n * Ring samples the shell gradient stack (base fill + wash) instead of a flat\n * `box-shadow`. Auto-enabled under an AppShell with an active gradient preset;\n * set `gradient-background` to opt in/out explicitly.\n *\n * The attribute must NOT start with `on` — Stencil's setAccessor routes any\n * unknown `on*` member down the event-listener path during attribute\n * reflection, calling addEventListener with a non-listener and throwing.\n */\n @Prop({ attribute: 'gradient-background', reflect: true }) gradientBackground: boolean = false;\n\n /** Contextual supplemental text. Omit when the owner hides the badge from assistive technology. */\n @Prop() label: string | undefined;\n\n /** Bumps on resize/layout so render recomputes gradient ring position. */\n @State() private gradientLayoutVersion = 0;\n\n private gradientObserver: ResizeObserver | null = null;\n private gradientWindowListener: (() => void) | null = null;\n\n componentDidLoad() {\n this.enableShellGradientRingIfNeeded();\n }\n\n disconnectedCallback() {\n this.unbindGradientRingSync();\n }\n\n @Watch('gradientBackground')\n @Watch('hasRing')\n gradientBackgroundChanged() {\n if (!this.hasRing) {\n this.unbindGradientRingSync();\n return;\n }\n if (!this.gradientBackground && isShellGradientActive(this.el)) {\n this.gradientBackground = true;\n return;\n }\n this.bindGradientRingSync();\n }\n\n /** Imperative opt-in avoids Stencil aborting parent render for nested badges. */\n private enableShellGradientRingIfNeeded() {\n if (!this.hasRing) return;\n\n if (!this.gradientBackground && isShellGradientActive(this.el)) {\n this.gradientBackground = true;\n }\n\n if (this.gradientBackground) {\n this.bindGradientRingSync();\n }\n }\n\n private bindGradientRingSync() {\n this.unbindGradientRingSync();\n\n if (!this.gradientBackground) return;\n\n const update = () => {\n this.gradientLayoutVersion += 1;\n };\n update();\n\n this.gradientWindowListener = update;\n window.addEventListener('resize', update);\n\n if (typeof ResizeObserver !== 'undefined') {\n this.gradientObserver = new ResizeObserver(update);\n this.gradientObserver.observe(this.el);\n\n const shell = this.el.closest('ds-app-shell');\n if (shell) this.gradientObserver.observe(shell);\n\n const bar = this.el.closest('ds-bar-nav');\n if (bar) this.gradientObserver.observe(bar);\n\n const panel = this.el.closest('ds-panel-nav');\n if (panel) this.gradientObserver.observe(panel);\n }\n }\n\n private unbindGradientRingSync() {\n if (this.gradientWindowListener) {\n window.removeEventListener('resize', this.gradientWindowListener);\n this.gradientWindowListener = null;\n }\n\n this.gradientObserver?.disconnect();\n this.gradientObserver = null;\n }\n\n private ringHostStyle(ring: string): Record<string, string> {\n void this.gradientLayoutVersion;\n\n const style: Record<string, string> = { '--_badge-ring': ring };\n if (!this.gradientBackground || !isShellGradientActive(this.el)) return style;\n\n /* Shell chrome wash uses background-attachment: fixed at viewport origin. */\n style[BADGE_GRADIENT_POSITION_VAR] = '0 0';\n return style;\n }\n\n render() {\n const isDot = this.variant === 'dot';\n\n if (!isDot && this.count <= 0) return <Host style={{ display: 'none' }} />;\n\n const display = this.count > this.max ? `${this.max}+` : String(this.count);\n const ring = this.background ?? SURFACE_RING[this.surface];\n\n return (\n <Host\n class={{\n badge: true,\n 'badge--counter': !isDot,\n 'badge--dot': isDot,\n 'badge--no-ring': !this.hasRing,\n 'badge--on-gradient-background': this.hasRing && this.gradientBackground,\n }}\n style={this.ringHostStyle(ring)}\n >\n <span class=\"badge__mark\" aria-hidden=\"true\">\n {!isDot && (\n <ds-text as=\"span\" variant=\"text-caption\" emphasis color=\"inherit\">\n {display}\n </ds-text>\n )}\n </span>\n {this.label && <span class=\"badge__a11y\">{this.label}</span>}\n </Host>\n );\n }\n}\n",
|
|
286
|
+
"type": "registry:ui"
|
|
287
|
+
}
|
|
288
|
+
]
|
|
289
|
+
}
|