@genesislcap/rapid-design-system 14.485.0 → 14.486.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,198 @@
1
+ import { baseLayerLuminance, configureRapidDesignSystem } from '../_config';
2
+ import { compileTheme } from './theme-compiler';
3
+ const STORAGE_KEY = 'foundation-ui:theme-modes';
4
+ const THEME_CLASS_PREFIX = 'theme-';
5
+ /** Applied luminance at or above this reads as "light" (LightMode = 1, DarkMode = 0.23). */
6
+ const LIGHT_LUMINANCE_THRESHOLD = 0.5;
7
+ const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
8
+ /** Deep-merge two design_tokens trees; `override` (the mode) wins at the leaves. */
9
+ const deepMerge = (base, override = {}) => {
10
+ const out = Object.assign({}, base);
11
+ for (const [key, value] of Object.entries(override)) {
12
+ const existing = out[key];
13
+ out[key] = isPlainObject(value) && isPlainObject(existing) ? deepMerge(existing, value) : value;
14
+ }
15
+ return out;
16
+ };
17
+ /**
18
+ * Narrow a theme loaded from JSON (or any untyped value) to a {@link ModalTheme},
19
+ * checking the structure a theme cannot function without. Prefer this over casting
20
+ * the JSON module: a malformed theme fails loudly here instead of silently
21
+ * rendering unthemed.
22
+ * @public
23
+ */
24
+ export const toModalTheme = (json) => {
25
+ if (!isPlainObject(json)) {
26
+ throw new Error('[modal-theme] A theme must be a plain JSON object.');
27
+ }
28
+ const modes = json.modes;
29
+ if (!isPlainObject(modes) || Object.keys(modes).length === 0) {
30
+ throw new Error('[modal-theme] A theme must declare at least one mode under "modes".');
31
+ }
32
+ const theme = json;
33
+ if (theme.defaultMode && !modes[theme.defaultMode]) {
34
+ console.warn(`[modal-theme] defaultMode "${theme.defaultMode}" is not a declared mode ` +
35
+ `(${Object.keys(modes).join(', ')}); the first declared mode will be used.`);
36
+ }
37
+ return theme;
38
+ };
39
+ // --- selection state (per-theme memory) -------------------------------------
40
+ const readModeMemory = () => {
41
+ var _a;
42
+ try {
43
+ const parsed = JSON.parse((_a = localStorage.getItem(STORAGE_KEY)) !== null && _a !== void 0 ? _a : '{}');
44
+ return isPlainObject(parsed) ? parsed : {};
45
+ }
46
+ catch (_b) {
47
+ return {};
48
+ }
49
+ };
50
+ const rememberMode = (themeId, mode) => {
51
+ const memory = readModeMemory();
52
+ memory[themeId] = mode;
53
+ try {
54
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(memory));
55
+ }
56
+ catch (_a) {
57
+ /* storage unavailable (private mode / quota) — selection is best-effort */
58
+ }
59
+ };
60
+ const themeKey = (theme) => { var _a; return (_a = theme.id) !== null && _a !== void 0 ? _a : 'default'; };
61
+ const modeNames = (theme) => { var _a; return Object.keys((_a = theme.modes) !== null && _a !== void 0 ? _a : {}); };
62
+ /** The declared default when it names a real mode, else the first declared mode. */
63
+ const fallbackMode = (theme) => { var _a; return theme.defaultMode && ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[theme.defaultMode]) ? theme.defaultMode : modeNames(theme)[0]; };
64
+ /**
65
+ * The mode to start on: remembered choice for this theme, else the declared default.
66
+ * @public
67
+ */
68
+ export const resolveInitialMode = (theme) => {
69
+ var _a;
70
+ const remembered = readModeMemory()[themeKey(theme)];
71
+ return remembered && ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[remembered]) ? remembered : fallbackMode(theme);
72
+ };
73
+ /**
74
+ * Cycle to the next mode (for the light/dark toggle; generalises to N modes).
75
+ * @public
76
+ */
77
+ export const nextMode = (theme, current) => {
78
+ const names = modeNames(theme);
79
+ const index = names.indexOf(current);
80
+ return names[(index + 1) % names.length];
81
+ };
82
+ /**
83
+ * Whether the app should offer a mode toggle. Defaults to "more than one mode exists";
84
+ * a `showModeToggle` on the theme overrides the inference. Use to set the header's
85
+ * toggle-button visibility.
86
+ * @public
87
+ */
88
+ export const isModeToggleEnabled = (theme) => { var _a; return (_a = theme.showModeToggle) !== null && _a !== void 0 ? _a : modeNames(theme).length > 1; };
89
+ // --- application ------------------------------------------------------------
90
+ // One adopted sheet per root (Document or ShadowRoot). Keying by the provider's root
91
+ // means a second provider in another root gets its own sheet (instead of silently
92
+ // rewriting the first one's), and a remounted provider that lands in an
93
+ // already-themed root updates the existing sheet in place. WeakMap keeps detached
94
+ // roots collectable, so tests and remounts need no explicit teardown.
95
+ const sheetByRoot = new WeakMap();
96
+ // The most recent applyMode target — what reapplyTheme (the HMR hook) re-applies.
97
+ let lastApplied = null;
98
+ /**
99
+ * Compile the theme to a single mode-agnostic sheet and adopt it into the scope
100
+ * that actually contains the provider. The sheet contains every mode; switching
101
+ * never regenerates it.
102
+ *
103
+ * The provider is typically rendered inside its host app's shadow root, not the
104
+ * document light DOM — so a document-scoped `rapid-design-system-provider { … }`
105
+ * rule would never match it. We adopt the sheet onto `provider.getRootNode()` (the
106
+ * containing ShadowRoot, or Document if the provider sits at document scope) so the
107
+ * selector resolves and the surface custom properties land on the provider,
108
+ * inheriting down through every nested shadow boundary.
109
+ *
110
+ * Re-entrant per root: the first call for a root adopts a new sheet; later calls
111
+ * for the same root update it in place (`replaceSync`) rather than adopting a
112
+ * duplicate — this is what lets a hot theme edit refresh styles without a reload.
113
+ * Providers sharing one root share its sheet (the compiled CSS targets the provider
114
+ * tag), so themes applied to the same root contend — give concurrently-themed
115
+ * providers their own shadow roots.
116
+ *
117
+ * @public
118
+ */
119
+ export const injectThemeStyles = (provider, theme) => {
120
+ const css = compileTheme(theme);
121
+ const root = provider.getRootNode();
122
+ // A detached provider's root is the provider itself (or a plain DocumentFragment),
123
+ // which cannot adopt stylesheets — surface an actionable diagnostic instead of
124
+ // crashing on the adoptedStyleSheets spread below.
125
+ if (!('adoptedStyleSheets' in root)) {
126
+ console.warn('[modal-theme] injectThemeStyles skipped: the provider is not connected to a Document ' +
127
+ 'or ShadowRoot. Connect the provider before applying the theme.');
128
+ return;
129
+ }
130
+ const existing = sheetByRoot.get(root);
131
+ if (existing) {
132
+ existing.replaceSync(css);
133
+ return;
134
+ }
135
+ const sheet = new CSSStyleSheet();
136
+ sheet.replaceSync(css);
137
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
138
+ sheetByRoot.set(root, sheet);
139
+ };
140
+ /**
141
+ * Recompile and re-apply the theme after a hot edit to the theme file: updates the
142
+ * adopted sheet in place and re-runs the active mode's design tokens on the most
143
+ * recently applied provider (falling back to the theme's initial mode if the edit
144
+ * removed the active one). Wire to a bundler's HMR hook so theme edits show without
145
+ * a full page reload.
146
+ *
147
+ * @public
148
+ */
149
+ export const reapplyTheme = (theme) => {
150
+ var _a;
151
+ if (!lastApplied)
152
+ return;
153
+ const { provider, mode } = lastApplied;
154
+ injectThemeStyles(provider, theme);
155
+ applyMode(provider, theme, ((_a = theme.modes) === null || _a === void 0 ? void 0 : _a[mode]) ? mode : resolveInitialMode(theme));
156
+ };
157
+ /**
158
+ * Apply a mode: design-token half via FAST, surface/parts half via the class swap.
159
+ * @public
160
+ */
161
+ export const applyMode = (provider, theme, mode) => {
162
+ var _a, _b, _c, _d, _e;
163
+ lastApplied = { provider, mode };
164
+ // (b) FAST recipe inputs — accent, luminance, neutral seed, typography, etc.
165
+ // The API/JSON key is snake_case (`design_tokens`); use a camelCase local to satisfy lint.
166
+ const designTokens = deepMerge(((_b = (_a = theme.shared) === null || _a === void 0 ? void 0 : _a.design_tokens) !== null && _b !== void 0 ? _b : {}), ((_e = (_d = (_c = theme.modes) === null || _c === void 0 ? void 0 : _c[mode]) === null || _d === void 0 ? void 0 : _d.design_tokens) !== null && _e !== void 0 ? _e : {}));
167
+ // configureDesignSystem merges the slice over the built-in defaults, so a partial
168
+ // tree is valid at runtime; the config type wants the full tree — widen here only.
169
+ configureRapidDesignSystem(provider, {
170
+ design_tokens: designTokens,
171
+ });
172
+ // (a) surfaces + parts are pure CSS — swap the mode class; the sheet does the rest.
173
+ for (const cls of Array.from(provider.classList)) {
174
+ if (cls.startsWith(THEME_CLASS_PREFIX))
175
+ provider.classList.remove(cls);
176
+ }
177
+ provider.classList.add(`${THEME_CLASS_PREFIX}${mode}`);
178
+ // Read the luminance FAST actually applied (the mode's tokens merged over the
179
+ // built-in defaults) — one source of truth, so the classes below can never
180
+ // disagree with the rendered surfaces, whatever the mode is named.
181
+ const isLight = baseLayerLuminance.getValueFor(provider) >= LIGHT_LUMINANCE_THRESHOLD;
182
+ // Back-compat: existing code/components may still key off the legacy `.light` class.
183
+ document.body.classList.toggle('light', isLight);
184
+ provider.classList.toggle('light', isLight);
185
+ // Keep the platform `luminance` key in sync with the actually-applied mode.
186
+ // foundation-header (the toggle icon) writes its own, often-inverse value
187
+ // synchronously right after emitting the toggle event, and grid-pro reads this
188
+ // key to theme grids — so we write in a microtask to land last and win.
189
+ queueMicrotask(() => {
190
+ try {
191
+ localStorage.setItem('luminance', isLight ? 'light' : 'dark');
192
+ }
193
+ catch (_a) {
194
+ /* storage unavailable — best effort */
195
+ }
196
+ });
197
+ rememberMode(themeKey(theme), mode);
198
+ };
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Compiles a modal theme file into class-scoped CSS (native nesting) for deployment.
3
+ *
4
+ * customTokens -> custom-property declarations
5
+ * componentOverrides -> nested rules keyed by any selector (::part(), a host tag, a class)
6
+ *
7
+ * Everything nests under one `rapid-design-system-provider` root. `shared` lives
8
+ * at the root (applies in every mode); each mode is a nested `&.theme-<name>`
9
+ * block whose compound selector out-specifies the root, so mode values win.
10
+ *
11
+ * design_tokens are NOT emitted: they are FAST recipe *inputs* consumed in JS
12
+ * (accent, luminance), applied at runtime via setValueFor (see modal-theme.ts).
13
+ *
14
+ * Input is validated, not escaped: keys, selectors, and values containing `{`/`}`
15
+ * throw (they would splice arbitrary rules into the sheet), and unknown section
16
+ * keys warn (they would otherwise be silently dropped).
17
+ *
18
+ * @public
19
+ */
20
+ const HOST = 'rapid-design-system-provider';
21
+ /** Section keys the theme runtime consumes; anything else is an authoring mistake. */
22
+ const KNOWN_SECTION_KEYS = new Set(['customTokens', 'componentOverrides', 'design_tokens']);
23
+ // A `{`/`}` inside a key, selector, or value terminates the current rule early and
24
+ // splices arbitrary rules into the adopted sheet — reject outright so an authoring
25
+ // typo fails loudly at compile time instead of silently corrupting the whole sheet.
26
+ // Semicolons are deliberately allowed: they occur in legitimate values (e.g.
27
+ // `url("data:image/svg+xml;base64,…")`) and can at most add a sibling declaration
28
+ // inside the same app-authored block — the theme author already controls whole
29
+ // property names, so `;` grants no capability braces don't already guard.
30
+ const assertBraceFree = (kind, text) => {
31
+ if (text.includes('{') || text.includes('}')) {
32
+ throw new Error(`[theme-compiler] Invalid ${kind} ${JSON.stringify(text)}: "{" and "}" are not allowed.`);
33
+ }
34
+ };
35
+ // Declaration values arrive from JSON, so numbers and booleans are representable even
36
+ // though the TS type says string — stringify those; reject shapes with no CSS form
37
+ // (objects/arrays/null would otherwise emit "[object Object]" silently).
38
+ const coerceDeclarationValue = (key, value) => {
39
+ if (typeof value === 'string')
40
+ return value;
41
+ if (typeof value === 'number' || typeof value === 'boolean')
42
+ return String(value);
43
+ throw new Error(`[theme-compiler] Invalid value for "${key}" ${JSON.stringify(value)}: expected a string, number, or boolean.`);
44
+ };
45
+ // The compiler silently ignores keys it doesn't read, which is exactly how a whole
46
+ // override block goes missing (e.g. the pre-rename `parts` key) — surface it.
47
+ const warnUnknownKeys = (sectionName, sectionObj) => {
48
+ for (const key of Object.keys(sectionObj)) {
49
+ if (!KNOWN_SECTION_KEYS.has(key)) {
50
+ console.warn(`[theme-compiler] Unknown key "${key}" in ${sectionName} is ignored — did you mean ` +
51
+ `"componentOverrides"? (supported: customTokens, componentOverrides, design_tokens)`);
52
+ }
53
+ }
54
+ };
55
+ const props = (map, pad) => Object.entries(map)
56
+ .map(([k, v]) => {
57
+ assertBraceFree('property name', k);
58
+ const value = coerceDeclarationValue(k, v);
59
+ assertBraceFree(`value of "${k}"`, value);
60
+ return `${pad}${k}: ${value};`;
61
+ })
62
+ .join('\n');
63
+ const overrideRules = (overrides, pad) => Object.entries(overrides)
64
+ .map(([sel, decls]) => {
65
+ assertBraceFree('selector', sel);
66
+ return `${pad}${sel} {\n${props(decls, pad + ' ')}\n${pad}}`;
67
+ })
68
+ .join('\n\n');
69
+ const section = (obj, pad) => {
70
+ const out = [];
71
+ if (obj.customTokens && Object.keys(obj.customTokens).length) {
72
+ out.push(props(obj.customTokens, pad));
73
+ }
74
+ if (obj.componentOverrides && Object.keys(obj.componentOverrides).length) {
75
+ out.push(overrideRules(obj.componentOverrides, pad));
76
+ }
77
+ return out.join('\n\n');
78
+ };
79
+ /**
80
+ * Compile a modal theme into class-scoped nested CSS.
81
+ *
82
+ * @param theme - the modal theme (shared + modes; customTokens + componentOverrides).
83
+ * @param host - the selector the rules nest under (default `rapid-design-system-provider`).
84
+ * @public
85
+ */
86
+ export function compileTheme(theme, { host = HOST } = {}) {
87
+ var _a, _b;
88
+ const body = [];
89
+ const sharedSection = (_a = theme.shared) !== null && _a !== void 0 ? _a : {};
90
+ warnUnknownKeys('shared', sharedSection);
91
+ const shared = section(sharedSection, ' ');
92
+ if (shared)
93
+ body.push(shared);
94
+ for (const [name, mode] of Object.entries((_b = theme.modes) !== null && _b !== void 0 ? _b : {})) {
95
+ assertBraceFree('mode name', name);
96
+ warnUnknownKeys(`mode "${name}"`, mode);
97
+ const modeContent = section(mode, ' ');
98
+ // Skip modes with no CSS to emit (e.g. adaptive modes that only carry
99
+ // design_tokens) so the sheet doesn't gain empty `&.theme-<name> {}` blocks.
100
+ if (modeContent) {
101
+ body.push(` &.theme-${name} {\n${modeContent}\n }`);
102
+ }
103
+ }
104
+ return `${host} {\n${body.join('\n\n')}\n}\n`;
105
+ }
@@ -15,5 +15,6 @@ export const rapidTabStyles = (context, definition) => css `
15
15
  :host([aria-selected='true']),
16
16
  :host([aria-selected='true']:hover) {
17
17
  background-color: var(--neutral-layer-4);
18
+ font-weight: 700;
18
19
  }
19
20
  `;
@@ -5,7 +5,8 @@ export const rapidTabPanelStyles = (context, definition) => css `
5
5
  :host {
6
6
  padding: 0;
7
7
  background-color: var(--neutral-layer-card-container);
8
- border-radius: var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
8
+ border-radius: 0 0 var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px))
9
+ var(--card-corner-radius, calc(var(--control-corner-radius) * 1.5px));
9
10
  border-style: solid;
10
11
  border-width: var(--tabs-border-width, 1px);
11
12
  border-color: var(--tabs-border-color, var(--neutral-stroke-rest));
package/dist/react.cjs CHANGED
@@ -112,11 +112,6 @@ const ActionsMenu = React.forwardRef(function ActionsMenu(props, ref) {
112
112
  return React.createElement(customElements.getName(ActionsMenuWC) ?? '%%prefix%%-actions-menu', { ...rest, ref }, children);
113
113
  });
114
114
 
115
- const AiIndicator = React.forwardRef(function AiIndicator(props, ref) {
116
- const { children, ...rest } = props;
117
- return React.createElement(customElements.getName(AiIndicatorWC) ?? '%%prefix%%-ai-indicator', { ...rest, ref }, children);
118
- });
119
-
120
115
  const AiCriteriaSearch = React.forwardRef(function AiCriteriaSearch(props, ref) {
121
116
  const { onCriteriaChanged, onValidationErrors, children, ...rest } = props;
122
117
  const _innerRef = React.useRef(null);
@@ -139,6 +134,11 @@ const AiCriteriaSearch = React.forwardRef(function AiCriteriaSearch(props, ref)
139
134
  return React.createElement(customElements.getName(AiCriteriaSearchWC) ?? '%%prefix%%-ai-criteria-search', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
140
135
  });
141
136
 
137
+ const AiIndicator = React.forwardRef(function AiIndicator(props, ref) {
138
+ const { children, ...rest } = props;
139
+ return React.createElement(customElements.getName(AiIndicatorWC) ?? '%%prefix%%-ai-indicator', { ...rest, ref }, children);
140
+ });
141
+
142
142
  const Anchor = React.forwardRef(function Anchor(props, ref) {
143
143
  const { children, ...rest } = props;
144
144
  return React.createElement(customElements.getName(AnchorWC) ?? '%%prefix%%-anchor', { ...rest, ref }, children);
@@ -925,8 +925,8 @@ module.exports = {
925
925
  Accordion,
926
926
  AccordionItem,
927
927
  ActionsMenu,
928
- AiIndicator,
929
928
  AiCriteriaSearch,
929
+ AiIndicator,
930
930
  Anchor,
931
931
  AnchoredRegion,
932
932
  Avatar,
package/dist/react.mjs CHANGED
@@ -110,11 +110,6 @@ export const ActionsMenu = React.forwardRef(function ActionsMenu(props, ref) {
110
110
  return React.createElement(customElements.getName(ActionsMenuWC) ?? '%%prefix%%-actions-menu', { ...rest, ref }, children);
111
111
  });
112
112
 
113
- export const AiIndicator = React.forwardRef(function AiIndicator(props, ref) {
114
- const { children, ...rest } = props;
115
- return React.createElement(customElements.getName(AiIndicatorWC) ?? '%%prefix%%-ai-indicator', { ...rest, ref }, children);
116
- });
117
-
118
113
  export const AiCriteriaSearch = React.forwardRef(function AiCriteriaSearch(props, ref) {
119
114
  const { onCriteriaChanged, onValidationErrors, children, ...rest } = props;
120
115
  const _innerRef = React.useRef(null);
@@ -137,6 +132,11 @@ export const AiCriteriaSearch = React.forwardRef(function AiCriteriaSearch(props
137
132
  return React.createElement(customElements.getName(AiCriteriaSearchWC) ?? '%%prefix%%-ai-criteria-search', { ...rest, ref: _mergeRefs(_innerRef, ref) }, children);
138
133
  });
139
134
 
135
+ export const AiIndicator = React.forwardRef(function AiIndicator(props, ref) {
136
+ const { children, ...rest } = props;
137
+ return React.createElement(customElements.getName(AiIndicatorWC) ?? '%%prefix%%-ai-indicator', { ...rest, ref }, children);
138
+ });
139
+
140
140
  export const Anchor = React.forwardRef(function Anchor(props, ref) {
141
141
  const { children, ...rest } = props;
142
142
  return React.createElement(customElements.getName(AnchorWC) ?? '%%prefix%%-anchor', { ...rest, ref }, children);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/rapid-design-system",
3
3
  "description": "Rapid Design System",
4
- "version": "14.485.0",
4
+ "version": "14.486.0",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -25,7 +25,9 @@
25
25
  "dev:tsc": "genx dev -b ts",
26
26
  "flexlayout:generate": "node ./scripts/generate-flexlayout-theme.mjs",
27
27
  "lint": "genx lint -l ox",
28
- "lint:fix": "genx lint -l ox --fix"
28
+ "lint:fix": "genx lint -l ox --fix",
29
+ "test": "genx test",
30
+ "test:unit:watch": "genx test --watch"
29
31
  },
30
32
  "madge": {
31
33
  "detectiveOptions": {
@@ -36,12 +38,13 @@
36
38
  },
37
39
  "devDependencies": {
38
40
  "@genesiscommunitysuccess/analyzer-import-alias-plugin": "^5.0.3",
39
- "@genesislcap/genx": "14.485.0",
40
- "@genesislcap/rollup-builder": "14.485.0",
41
- "@genesislcap/ts-builder": "14.485.0",
42
- "@genesislcap/uvu-playwright-builder": "14.485.0",
43
- "@genesislcap/vite-builder": "14.485.0",
44
- "@genesislcap/webpack-builder": "14.485.0",
41
+ "@genesislcap/foundation-testing": "14.486.0",
42
+ "@genesislcap/genx": "14.486.0",
43
+ "@genesislcap/rollup-builder": "14.486.0",
44
+ "@genesislcap/ts-builder": "14.486.0",
45
+ "@genesislcap/uvu-playwright-builder": "14.486.0",
46
+ "@genesislcap/vite-builder": "14.486.0",
47
+ "@genesislcap/webpack-builder": "14.486.0",
45
48
  "flexlayout-react": "^0.8.19"
46
49
  },
47
50
  "peerDependencies": {
@@ -53,9 +56,9 @@
53
56
  }
54
57
  },
55
58
  "dependencies": {
56
- "@genesislcap/foundation-logger": "14.485.0",
57
- "@genesislcap/foundation-ui": "14.485.0",
58
- "@genesislcap/foundation-utils": "14.485.0",
59
+ "@genesislcap/foundation-logger": "14.486.0",
60
+ "@genesislcap/foundation-ui": "14.486.0",
61
+ "@genesislcap/foundation-utils": "14.486.0",
59
62
  "@microsoft/fast-colors": "5.3.1",
60
63
  "@microsoft/fast-components": "2.30.6",
61
64
  "@microsoft/fast-element": "1.14.0",
@@ -83,5 +86,5 @@
83
86
  "require": "./dist/react.cjs"
84
87
  }
85
88
  },
86
- "gitHead": "6d8eacc0597e7a9a323fa3838c493e7ac8ddc589"
89
+ "gitHead": "5de11f8989291fe8aaf19a7048dd6be68c0be9c8"
87
90
  }