@underpostnet/cyberia 3.3.77 → 3.4.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/.env.example +1 -0
- package/.github/workflows/coverall.cyberia.ci.yml +12 -3
- package/.github/workflows/cyberia-client.cd.yml +2 -1
- package/.github/workflows/cyberia-server.cd.yml +2 -1
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +6 -0
- package/.github/workflows/ghpkg.ci.yml +19 -2
- package/.github/workflows/gitlab.ci.yml +8 -6
- package/.github/workflows/hardhat.ci.yml +4 -1
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/.github/workflows/publish.ci.yml +21 -12
- package/.github/workflows/publish.cyberia.ci.yml +25 -14
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -2
- package/CHANGELOG.md +75 -140
- package/CLI-HELP.md +28 -1
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/README.md +1 -1
- package/bin/build.js +2 -4
- package/bin/cyberia.js +4 -4
- package/bin/index.js +4 -4
- package/compose.env +1 -0
- package/conf.js +1 -1042
- package/deploy/cyberia-client/deploy.sh +13 -11
- package/deploy/cyberia-server/deploy.sh +13 -11
- package/deploy/dd-cyberia/sync-deploy.sh +7 -28
- package/deploy/lib/config.sh +1 -1
- package/deploy/lib/github-actions-logging.sh +3 -2
- package/deploy/pwa-microservices-template/deploy.sh +7 -1
- package/deployment.yaml +1 -1
- package/docker-compose.yml +7 -7
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1 -1
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +1 -1
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +1 -1
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +1 -1
- package/docs/coverage/cyberia/api/object-layer/index.html +1 -1
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1 -1
- package/docs/coverage/cyberia/index.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/index.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +1 -1
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +1 -1
- package/hardhat/package-lock.json +6 -6
- package/hardhat/package.json +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +19 -17
- package/scripts/test-monitor.sh +1 -1
- package/src/api/file/file.router.js +7 -1
- package/src/cli/baremetal.js +1 -1
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/index.js +36 -0
- package/src/cli/kubectl.js +4 -3
- package/src/cli/release.js +2 -0
- package/src/cli/repository.js +7 -2
- package/src/cli/run.js +1 -1
- package/src/cli/secrets.js +393 -200
- package/src/cli/socketsecurity.js +260 -0
- package/src/client/components/core/CssCore.js +3 -0
- package/src/client/components/core/Modal.js +160 -36
- package/src/client/components/core/SearchBox.js +0 -1
- package/src/client/components/underpost/AppShellUnderpost.js +13 -1
- package/src/client/components/underpost/CssUnderpost.js +15 -2
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +8 -0
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +54 -56
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +30 -29
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +54 -25
- package/src/client/ssr/head/Css.js +12 -10
- package/src/client/ssr/views/Cyberia404.js +6 -10
- package/src/client/ssr/views/NotFound.js +75 -0
- package/src/client-builder/client-bundle.js +2 -2
- package/src/index.js +13 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/compose.env +1 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
- package/src/server/build/catalog.js +1 -0
- package/src/server/build/execution.js +19 -5
- package/src/server/build/package.js +166 -3
- package/src/server/build/testing.js +2 -1
- package/src/server/runtime/conf.js +21 -0
- package/src/server/runtime/config-scope.js +2 -0
- package/src/server/runtime/process.js +4 -3
- package/src/server/security/socketsecurity.js +1735 -0
- package/test/integration/infra/1-security/config-scope.test.js +3 -0
- package/test/integration/infra/1-security/socketsecurity.test.js +1041 -0
- package/test/integration/infra/1-security/sops-secret-store.test.js +201 -71
- package/test/unit/build-template.test.js +24 -0
- package/test/unit/deploy-log-table.test.js +38 -0
- package/test/unit/execution-profiles.test.js +1 -0
- package/test/unit/package.test.js +118 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Socket security domain: the security audit of a checkout — the packages it installs and its own
|
|
3
|
+
* source code — and the patches it carries.
|
|
4
|
+
*
|
|
5
|
+
* {@link module:src/server/security/socketsecurity.js} owns the Socket CLI integration and the
|
|
6
|
+
* report; this domain is the operator surface over it: the audit a person or a workflow runs,
|
|
7
|
+
* the remediation pass, and the `prepare` hook that replays the patch manifest.
|
|
8
|
+
* @module src/cli/socketsecurity.js
|
|
9
|
+
* @namespace UnderpostSocketSecurity
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import nodePath from 'node:path';
|
|
14
|
+
import { auditRuntimeDependencies } from '../server/build/package.js';
|
|
15
|
+
import { loggerFactory } from '../server/ops/logger.js';
|
|
16
|
+
import SocketSecurityService, {
|
|
17
|
+
fix,
|
|
18
|
+
npmAudit,
|
|
19
|
+
packageAlerts,
|
|
20
|
+
npmAuditFix,
|
|
21
|
+
patchApply,
|
|
22
|
+
patchGet,
|
|
23
|
+
patchManifestFactory,
|
|
24
|
+
patchScan,
|
|
25
|
+
patchSelectionFactory,
|
|
26
|
+
patchSetup,
|
|
27
|
+
publishedPackagesFactory,
|
|
28
|
+
reportViolates,
|
|
29
|
+
scan,
|
|
30
|
+
securityReportFactory,
|
|
31
|
+
socketEnvFactory,
|
|
32
|
+
socketVersion,
|
|
33
|
+
writeSecurityReports,
|
|
34
|
+
} from '../server/security/socketsecurity.js';
|
|
35
|
+
import Underpost from '../index.js';
|
|
36
|
+
|
|
37
|
+
const logger = loggerFactory(import.meta);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @class UnderpostSocketSecurity
|
|
41
|
+
* @description Audits a checkout's dependencies and source code through Socket, and remediates and patches its dependencies.
|
|
42
|
+
* @memberof UnderpostSocketSecurity
|
|
43
|
+
*/
|
|
44
|
+
class UnderpostSocketSecurity {
|
|
45
|
+
static API = {
|
|
46
|
+
/**
|
|
47
|
+
* @method audit
|
|
48
|
+
* @description Runs the audit and writes its reports. Dependency security: segregation,
|
|
49
|
+
* `npm audit`, the Socket patch scan and, with an API token, the Socket full scan with the
|
|
50
|
+
* local reachability analysis when asked. Source code risk: with an API token, the alerts
|
|
51
|
+
* Socket's registry analysis raises on this project's own published package, as refactoring
|
|
52
|
+
* tasks at their locations.
|
|
53
|
+
* @param {object} [params]
|
|
54
|
+
* @param {string} [params.root] - Checkout to audit.
|
|
55
|
+
* @param {string} [params.out] - Report directory.
|
|
56
|
+
* @param {boolean} [params.reach] - Run the reachability analysis with the scan.
|
|
57
|
+
* @param {object} [params.remediation] - Actions a remediation pass took before this audit.
|
|
58
|
+
* @returns {Promise<object>} The report, with the files written.
|
|
59
|
+
* @memberof UnderpostSocketSecurity
|
|
60
|
+
*/
|
|
61
|
+
async audit({
|
|
62
|
+
root = process.cwd(),
|
|
63
|
+
out = nodePath.join(root, SocketSecurityService.REPORT_DIRECTORY),
|
|
64
|
+
reach = false,
|
|
65
|
+
remediation = {},
|
|
66
|
+
} = {}) {
|
|
67
|
+
const env = socketEnvFactory();
|
|
68
|
+
const dependencies = await auditRuntimeDependencies({ root });
|
|
69
|
+
const audit = npmAudit({ root });
|
|
70
|
+
const patches = patchScan({ root, env });
|
|
71
|
+
if (reach && !env.token)
|
|
72
|
+
logger.warn('Reachability analysis needs a Socket API token', { variable: SocketSecurityService.ENV.token });
|
|
73
|
+
const scanResult = env.token ? scan({ root, env, reach }) : null;
|
|
74
|
+
const packageJson = fs.readJsonSync(nodePath.join(root, 'package.json'));
|
|
75
|
+
const ownPackages = SocketSecurityService.ownPackageNamesFactory({ packageJson });
|
|
76
|
+
const ownAlerts = env.token
|
|
77
|
+
? packageAlerts({ root, env, packages: publishedPackagesFactory({ root, packageJson, ownPackages }) })
|
|
78
|
+
: null;
|
|
79
|
+
const report = securityReportFactory({
|
|
80
|
+
root,
|
|
81
|
+
packageJson,
|
|
82
|
+
ownPackages,
|
|
83
|
+
dependencies,
|
|
84
|
+
npmAudit: audit.summary,
|
|
85
|
+
scan: scanResult,
|
|
86
|
+
packageAlerts: ownAlerts,
|
|
87
|
+
patchScan: patches,
|
|
88
|
+
manifest: patchManifestFactory(root),
|
|
89
|
+
remediation,
|
|
90
|
+
env,
|
|
91
|
+
cliVersion: socketVersion({ root }),
|
|
92
|
+
});
|
|
93
|
+
const files = writeSecurityReports({
|
|
94
|
+
report,
|
|
95
|
+
directory: out,
|
|
96
|
+
artifacts: {
|
|
97
|
+
npmAudit: audit.raw,
|
|
98
|
+
scan: scanResult?.report ?? null,
|
|
99
|
+
scanArtifacts: scanResult?.artifacts ?? null,
|
|
100
|
+
packageAlerts: ownAlerts?.artifacts ?? null,
|
|
101
|
+
reach: scanResult?.facts ?? null,
|
|
102
|
+
patchScan: patches.data,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const { dependencySecurity: dependency, sourceCodeRisk: source } = report;
|
|
106
|
+
logger.info('Security audit complete', {
|
|
107
|
+
scan: report.scan.skipped ?? { healthy: report.scan.healthy, url: report.scan.url },
|
|
108
|
+
dependencySecurity: {
|
|
109
|
+
findings: dependency.counts,
|
|
110
|
+
misplacedDependencies: dependency.segregation.misplaced.map(({ name }) => name),
|
|
111
|
+
patchesAvailable: dependency.patches.scan.available.length,
|
|
112
|
+
patchesInManifest: dependency.patches.manifest.patches.length,
|
|
113
|
+
deferred: dependency.deferred.length,
|
|
114
|
+
},
|
|
115
|
+
sourceCodeRisk: {
|
|
116
|
+
status: source.status,
|
|
117
|
+
analyzed: source.analyzed.map(({ name, version }) => `${name}@${version}`),
|
|
118
|
+
findings: source.counts,
|
|
119
|
+
tasks: source.tasks.map(({ label, locations }) => `${label}: ${locations.length}`),
|
|
120
|
+
},
|
|
121
|
+
files,
|
|
122
|
+
});
|
|
123
|
+
return { ...report, files };
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @method remediate
|
|
128
|
+
* @description The non-breaking remediation pass: `npm audit fix`, every available Socket
|
|
129
|
+
* patch, `socket fix` without major upgrades unless allowed, and a final manifest apply.
|
|
130
|
+
* @param {object} [params]
|
|
131
|
+
* @param {string} [params.root] - Checkout to remediate.
|
|
132
|
+
* @param {string} [params.out] - Directory `socket fix` writes its upgrade list into.
|
|
133
|
+
* @param {boolean} [params.majorUpdates] - Let `socket fix` apply major upgrades.
|
|
134
|
+
* @returns {Promise<object>} Results by action name.
|
|
135
|
+
* @memberof UnderpostSocketSecurity
|
|
136
|
+
*/
|
|
137
|
+
async remediate({
|
|
138
|
+
root = process.cwd(),
|
|
139
|
+
out = nodePath.join(root, SocketSecurityService.REPORT_DIRECTORY),
|
|
140
|
+
majorUpdates = false,
|
|
141
|
+
} = {}) {
|
|
142
|
+
const env = socketEnvFactory();
|
|
143
|
+
fs.ensureDirSync(out);
|
|
144
|
+
const actions = { 'npm audit fix': npmAuditFix({ root }) };
|
|
145
|
+
const { selected, skipped } = patchSelectionFactory(patchScan({ root, env }).summary.available);
|
|
146
|
+
actions['socket patch get'] = [
|
|
147
|
+
...selected.map(({ uuid, purl }) => {
|
|
148
|
+
const { ok, message } = patchGet({ root, env, identifier: uuid });
|
|
149
|
+
return { uuid, purl, ok, message };
|
|
150
|
+
}),
|
|
151
|
+
...skipped.map(({ uuid, purl, reason }) => ({ uuid, purl, skipped: reason })),
|
|
152
|
+
];
|
|
153
|
+
actions['socket fix'] = env.token
|
|
154
|
+
? fix({
|
|
155
|
+
root,
|
|
156
|
+
env,
|
|
157
|
+
majorUpdates,
|
|
158
|
+
outputFile: nodePath.join(out, SocketSecurityService.REPORT_FILES.fixUpgrades),
|
|
159
|
+
})
|
|
160
|
+
: { skipped: `${SocketSecurityService.ENV.token} is not set` };
|
|
161
|
+
actions['socket patch apply'] = patchApply({ root, env });
|
|
162
|
+
logger.info('Remediation pass complete', actions);
|
|
163
|
+
return actions;
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @method applyPatches
|
|
168
|
+
* @description Applies the patch manifest; the `prepare` hook. A host without the
|
|
169
|
+
* Socket CLI and a checkout without a manifest are both no-ops, a patch that fails to apply
|
|
170
|
+
* fails the install.
|
|
171
|
+
* @param {object} [params]
|
|
172
|
+
* @param {string} [params.root] - Checkout to patch.
|
|
173
|
+
* @param {boolean} [params.dryRun] - Verify without writing.
|
|
174
|
+
* @returns {object} The apply result.
|
|
175
|
+
* @throws {Error} When a recorded patch does not apply.
|
|
176
|
+
* @memberof UnderpostSocketSecurity
|
|
177
|
+
*/
|
|
178
|
+
applyPatches({ root = process.cwd(), dryRun = false } = {}) {
|
|
179
|
+
const result = patchApply({ root, dryRun });
|
|
180
|
+
if (result.unavailable) {
|
|
181
|
+
logger.info('Socket CLI not installed; patch manifest not applied', {
|
|
182
|
+
install: `npm install -g ${SocketSecurityService.CLI_PACKAGE}`,
|
|
183
|
+
});
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
186
|
+
if (!result.ok || result.failed > 0)
|
|
187
|
+
throw new Error(
|
|
188
|
+
`[socketsecurity] patch apply failed: ${result.message || `${result.failed} patch(es) failed`}`,
|
|
189
|
+
);
|
|
190
|
+
logger.info(result.status === 'no_manifest' ? 'No patch manifest to apply' : 'Patch manifest applied', {
|
|
191
|
+
manifest: SocketSecurityService.MANIFEST_PATH,
|
|
192
|
+
patchesApplied: result.patchesApplied,
|
|
193
|
+
alreadyPatched: result.alreadyPatched,
|
|
194
|
+
dryRun,
|
|
195
|
+
});
|
|
196
|
+
return result;
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @method callback
|
|
201
|
+
* @description `underpost socketsecurity` entrypoint. Without an operation flag it runs the
|
|
202
|
+
* audit.
|
|
203
|
+
* @param {object} [options]
|
|
204
|
+
* @param {boolean} [options.audit] - Run the audit and write the reports.
|
|
205
|
+
* @param {boolean} [options.reach] - Include the reachability analysis in the scan.
|
|
206
|
+
* @param {boolean} [options.ci] - Audit, then fail on a policy violation or a finding at `--fail-on`.
|
|
207
|
+
* @param {string} [options.failOn] - Severity `--ci` fails at.
|
|
208
|
+
* @param {boolean} [options.fix] - Run the remediation pass before the audit.
|
|
209
|
+
* @param {boolean} [options.major] - Let the remediation pass apply major upgrades.
|
|
210
|
+
* @param {boolean} [options.patchScan] - Scan the installed packages for available patches.
|
|
211
|
+
* @param {string} [options.patchGet] - Download and apply one patch.
|
|
212
|
+
* @param {boolean} [options.patchApply] - Apply the patch manifest.
|
|
213
|
+
* @param {boolean} [options.patchSetup] - Install the `prepare` hook.
|
|
214
|
+
* @param {string} [options.out] - Report directory.
|
|
215
|
+
* @param {boolean} [options.dryRun] - Preview `--patch-setup`; verify only for `--patch-apply`.
|
|
216
|
+
* @returns {Promise<object>} The operation result.
|
|
217
|
+
* @memberof UnderpostSocketSecurity
|
|
218
|
+
*/
|
|
219
|
+
async callback(options = {}) {
|
|
220
|
+
const root = process.cwd();
|
|
221
|
+
const out = nodePath.resolve(root, options.out || SocketSecurityService.REPORT_DIRECTORY);
|
|
222
|
+
const dryRun = options.dryRun === true;
|
|
223
|
+
|
|
224
|
+
if (options.patchSetup) {
|
|
225
|
+
const result = patchSetup({ root, dryRun });
|
|
226
|
+
logger.info(
|
|
227
|
+
result.changed ? `prepare hook ${dryRun ? 'to install' : 'installed'}` : 'prepare hook present',
|
|
228
|
+
result,
|
|
229
|
+
);
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
if (options.patchApply) return Underpost.socketSecurity.applyPatches({ root, dryRun });
|
|
233
|
+
if (options.patchGet) {
|
|
234
|
+
const result = patchGet({ root, identifier: options.patchGet });
|
|
235
|
+
if (!result.ok) throw new Error(`[socketsecurity] patch get failed: ${result.message}`);
|
|
236
|
+
logger.info('Socket patch recorded and applied', result.data);
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
if (options.patchScan) {
|
|
240
|
+
const result = patchScan({ root });
|
|
241
|
+
if (!result.ok) throw new Error(`[socketsecurity] patch scan failed: ${result.message}`);
|
|
242
|
+
logger.info('Socket patches available', result.summary);
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const remediation = options.fix
|
|
247
|
+
? await Underpost.socketSecurity.remediate({ root, out, majorUpdates: options.major === true })
|
|
248
|
+
: {};
|
|
249
|
+
const report = await Underpost.socketSecurity.audit({ root, out, reach: options.reach === true, remediation });
|
|
250
|
+
if (options.ci && reportViolates(report, options.failOn))
|
|
251
|
+
throw new Error(
|
|
252
|
+
`[socketsecurity] policy gate failed (fail-on ${options.failOn || 'high'}); ` +
|
|
253
|
+
`see ${nodePath.join(out, SocketSecurityService.REPORT_FILES.markdown)}`,
|
|
254
|
+
);
|
|
255
|
+
return report;
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export default UnderpostSocketSecurity;
|
|
@@ -129,6 +129,9 @@ class Modal {
|
|
|
129
129
|
/** @type {Object.<string, ModalDataEntry>} */
|
|
130
130
|
static Data = {};
|
|
131
131
|
|
|
132
|
+
/** @type {Function|null} Recomputes visibility of the search/home toggle button; set once the slide-menu modal renders it. */
|
|
133
|
+
static updateBarCustomVisibility = null;
|
|
134
|
+
|
|
132
135
|
/**
|
|
133
136
|
* Bottom offset of the slide-menu area for the given options.
|
|
134
137
|
* @param {ModalRenderOptions} options
|
|
@@ -228,6 +231,10 @@ class Modal {
|
|
|
228
231
|
const originHeightTopBar = 50;
|
|
229
232
|
options.heightBottomBar = 0;
|
|
230
233
|
options.heightTopBar = 100;
|
|
234
|
+
// A view sits under its slide menu's bars, so it follows that menu's bar mode unless it
|
|
235
|
+
// names one — otherwise a 'top-bottom-bar' shell opens every view 100px down instead of 50px.
|
|
236
|
+
if (options && !options.barMode && options.slideMenu && Modal.Data[options.slideMenu]?.options?.barMode)
|
|
237
|
+
options.barMode = Modal.Data[options.slideMenu].options.barMode;
|
|
231
238
|
if (options && options.barMode && options.barMode === 'top-bottom-bar') {
|
|
232
239
|
options.heightTopBar = 50;
|
|
233
240
|
options.heightBottomBar = 50;
|
|
@@ -297,6 +304,9 @@ class Modal {
|
|
|
297
304
|
route: options.route,
|
|
298
305
|
RouterInstance: options.RouterInstance,
|
|
299
306
|
});
|
|
307
|
+
// Path just changed away from (or back to) home — recompute the search/home
|
|
308
|
+
// toggle's visibility instead of leaving it stuck on its last computed state.
|
|
309
|
+
setTimeout(() => Modal.updateBarCustomVisibility?.());
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
break;
|
|
@@ -319,10 +329,15 @@ class Modal {
|
|
|
319
329
|
padding: 5px;
|
|
320
330
|
}
|
|
321
331
|
.default-slide-menu-top-bar-fix-title-container-text {
|
|
322
|
-
font-size: 26px;
|
|
323
|
-
top: 8px;
|
|
324
332
|
color: ${darkTheme ? '#ffffff' : '#000000'};
|
|
325
333
|
}
|
|
334
|
+
/* Themes size and offset the title for the 100px banner with !important;
|
|
335
|
+
the 50px one has to win over them or the title spills into the menu. */
|
|
336
|
+
.slide-menu-top-bar-fix .default-slide-menu-top-bar-fix-title-container-text {
|
|
337
|
+
font-size: 26px !important;
|
|
338
|
+
top: 8px !important;
|
|
339
|
+
left: 0px !important;
|
|
340
|
+
}
|
|
326
341
|
</style>`;
|
|
327
342
|
} else {
|
|
328
343
|
style = html`<style>
|
|
@@ -428,6 +443,28 @@ class Modal {
|
|
|
428
443
|
s('body').classList[collapsed ? 'add' : 'remove']('main-btn-menu-top-fixed-active');
|
|
429
444
|
s('body').classList[collapsed && menuClosed ? 'add' : 'remove']('main-btn-menu-top-fixed-title-offset');
|
|
430
445
|
};
|
|
446
|
+
// Hides the search/home toggle only while it's showing the "close" (home) icon
|
|
447
|
+
// AND the body is already scrolled to top — nothing left for it to do there.
|
|
448
|
+
// Reactive: called on every scroll tick and state change, so it restores to its
|
|
449
|
+
// normal visible state the moment either condition stops holding.
|
|
450
|
+
const updateBarCustomVisibility = () => {
|
|
451
|
+
const barCustom = s(`.main-body-btn-bar-custom`);
|
|
452
|
+
if (!barCustom || !options?.slideMenuTopBarBannerFix) return;
|
|
453
|
+
const mainBody = s(`.main-body`);
|
|
454
|
+
const atTop = !!mainBody && mainBody.scrollTop <= 0;
|
|
455
|
+
const bannerFix = s(`.slide-menu-top-bar-fix`);
|
|
456
|
+
const bannerVisible = !!bannerFix && parseInt(bannerFix.style.top, 10) >= 0;
|
|
457
|
+
const isHomePath = location.pathname === getProxyPath();
|
|
458
|
+
// Never hide while the open (magnifying-glass) icon is showing — that's the
|
|
459
|
+
// idle "click to search" state, not the "close/home" state this hide targets.
|
|
460
|
+
const openIconHidden =
|
|
461
|
+
!!s(`.main-body-btn-ui-bar-custom-open`) &&
|
|
462
|
+
s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide');
|
|
463
|
+
barCustom.classList[bannerVisible && isHomePath && atTop && openIconHidden ? 'add' : 'remove']('hide');
|
|
464
|
+
};
|
|
465
|
+
// Exposed statically so other code paths (e.g. opening a view/route) can force a
|
|
466
|
+
// recompute — otherwise the hidden state goes stale once the path stops being home.
|
|
467
|
+
Modal.updateBarCustomVisibility = updateBarCustomVisibility;
|
|
431
468
|
barConfig.buttons.menu.onClick = () => {
|
|
432
469
|
Modal.Data[idModal][options.mode].width = slideMenuWidth;
|
|
433
470
|
s(`.btn-menu-${idModal}`).classList.add('hide');
|
|
@@ -452,6 +489,10 @@ class Modal {
|
|
|
452
489
|
}
|
|
453
490
|
syncFixedMenuButton();
|
|
454
491
|
Responsive.triggerChanged(`slide-menu-${idModal}`);
|
|
492
|
+
if (options.onExtendMenu) options.onExtendMenu();
|
|
493
|
+
Object.keys(this.Data[idModal].onExtendMenuListener).map((keyListener) =>
|
|
494
|
+
this.Data[idModal].onExtendMenuListener[keyListener](),
|
|
495
|
+
);
|
|
455
496
|
};
|
|
456
497
|
barConfig.buttons.close.onClick = () => {
|
|
457
498
|
Modal.Data[idModal][options.mode].width = 0;
|
|
@@ -476,6 +517,10 @@ class Modal {
|
|
|
476
517
|
}
|
|
477
518
|
syncFixedMenuButton();
|
|
478
519
|
Responsive.triggerChanged(`slide-menu-${idModal}`);
|
|
520
|
+
if (options.onCollapseMenu) options.onCollapseMenu();
|
|
521
|
+
Object.keys(this.Data[idModal].onCollapseMenuListener).map((keyListener) =>
|
|
522
|
+
this.Data[idModal].onCollapseMenuListener[keyListener](),
|
|
523
|
+
);
|
|
479
524
|
};
|
|
480
525
|
transition += `, width 0.3s`;
|
|
481
526
|
|
|
@@ -532,14 +577,11 @@ class Modal {
|
|
|
532
577
|
Modal.actionBtnCenter();
|
|
533
578
|
};
|
|
534
579
|
|
|
535
|
-
// Fixed top-left hamburger, shown only while the
|
|
536
|
-
// (main-body-btn-ui-close hidden)
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
options.mode !== 'slide-menu-right' &&
|
|
541
|
-
options.barMode !== 'top-bottom-bar'
|
|
542
|
-
) {
|
|
580
|
+
// Fixed top-left hamburger, shown only while the bars are collapsed
|
|
581
|
+
// (main-body-btn-ui-close hidden): it stands in for the left menu hamburger the
|
|
582
|
+
// collapse hid — the top bar's, or the bottom bar's under 'top-bottom-bar'. A right
|
|
583
|
+
// menu keeps its floating .main-body-btn-menu beside the menu edge instead.
|
|
584
|
+
if (idModal === 'modal-menu' && options.mode !== 'slide-menu-right') {
|
|
543
585
|
append(
|
|
544
586
|
'body',
|
|
545
587
|
html`
|
|
@@ -587,19 +629,38 @@ class Modal {
|
|
|
587
629
|
s(`.main-body-btn-ui-bar-custom-open`).classList.remove('hide');
|
|
588
630
|
s(`.main-body-btn-ui-bar-custom-close`).classList.add('hide');
|
|
589
631
|
s(`.slide-menu-top-bar-fix`).style.top = '0px';
|
|
632
|
+
|
|
633
|
+
setTimeout(async () => {
|
|
634
|
+
setTimeout(() => {
|
|
635
|
+
s(`.action-btn-home`).click();
|
|
636
|
+
});
|
|
637
|
+
const mainBody = s(`.main-body`);
|
|
638
|
+
if (mainBody && mainBody.scrollTop > 0) {
|
|
639
|
+
mainBody.scrollTo({ top: 0, behavior: 'smooth' });
|
|
640
|
+
}
|
|
641
|
+
updateBarCustomVisibility();
|
|
642
|
+
});
|
|
590
643
|
} else {
|
|
591
644
|
s(`.main-body-btn-ui-bar-custom-open`).classList.add('hide');
|
|
592
645
|
s(`.main-body-btn-ui-bar-custom-close`).classList.remove('hide');
|
|
593
646
|
s(`.slide-menu-top-bar-fix`).style.top = '-100px';
|
|
594
647
|
s(`.top-bar-search-box-container`).click();
|
|
595
648
|
}
|
|
649
|
+
updateBarCustomVisibility();
|
|
596
650
|
if (Modal.mobileModal()) {
|
|
597
651
|
btnCloseEvent();
|
|
598
652
|
}
|
|
599
653
|
};
|
|
600
654
|
|
|
601
|
-
let _heightTopBar, _heightBottomBar, _topMenu;
|
|
655
|
+
let _heightTopBar, _heightBottomBar, _topMenu, _barsAnimationTimeout;
|
|
656
|
+
// The layout offsets below are set inline, so they only ease while this class is on.
|
|
657
|
+
const animateBarsToggle = () => {
|
|
658
|
+
s('body').classList.add('ui-bars-animating');
|
|
659
|
+
clearTimeout(_barsAnimationTimeout);
|
|
660
|
+
_barsAnimationTimeout = setTimeout(() => s('body').classList.remove('ui-bars-animating'), 300);
|
|
661
|
+
};
|
|
602
662
|
s(`.main-body-btn-ui`).onclick = () => {
|
|
663
|
+
animateBarsToggle();
|
|
603
664
|
if (s(`.main-body-btn-ui-open`).classList.contains('hide')) {
|
|
604
665
|
s(`.main-body-btn-ui-open`).classList.remove('hide');
|
|
605
666
|
s(`.main-body-btn-ui-close`).classList.add('hide');
|
|
@@ -608,8 +669,7 @@ class Modal {
|
|
|
608
669
|
_topMenu = newInstance(s(`.modal-menu`).style.top);
|
|
609
670
|
options.heightTopBar = 0;
|
|
610
671
|
options.heightBottomBar = 0;
|
|
611
|
-
s(
|
|
612
|
-
s(`.bottom-bar`).classList.add('hide');
|
|
672
|
+
s('body').classList.add('ui-bars-collapsed');
|
|
613
673
|
syncFixedMenuButton();
|
|
614
674
|
s(`.modal-menu`).style.top = '0px';
|
|
615
675
|
s(`.main-body-btn-container`).style.top = '50px';
|
|
@@ -624,8 +684,7 @@ class Modal {
|
|
|
624
684
|
options.heightBottomBar = _heightBottomBar;
|
|
625
685
|
s(`.modal-menu`).style.top = _topMenu;
|
|
626
686
|
s(`.main-body-btn-container`).style.top = `${options.heightTopBar + 50}px`;
|
|
627
|
-
s(
|
|
628
|
-
s(`.bottom-bar`).classList.remove('hide');
|
|
687
|
+
s('body').classList.remove('ui-bars-collapsed');
|
|
629
688
|
syncFixedMenuButton();
|
|
630
689
|
s(`.main-body`).style.top = `${options.heightTopBar}px`;
|
|
631
690
|
s(`.main-body`).style.height = `${windowGetH() - options.heightTopBar}px`;
|
|
@@ -649,13 +708,62 @@ class Modal {
|
|
|
649
708
|
);
|
|
650
709
|
}
|
|
651
710
|
};
|
|
711
|
+
updateBarCustomVisibility();
|
|
652
712
|
Modal.setTopBannerLink();
|
|
653
713
|
});
|
|
654
714
|
|
|
655
715
|
const inputSearchBoxId = `top-bar-search-box`;
|
|
656
716
|
append(
|
|
657
717
|
'body',
|
|
658
|
-
html
|
|
718
|
+
html`<style>
|
|
719
|
+
/* Collapsing slides the bars off-screen instead of removing them; they turn
|
|
720
|
+
invisible only once the slide ends, so no hidden control stays focusable. */
|
|
721
|
+
.slide-menu-top-bar,
|
|
722
|
+
.bottom-bar {
|
|
723
|
+
transition:
|
|
724
|
+
transform 0.3s ease,
|
|
725
|
+
opacity 0.3s ease,
|
|
726
|
+
visibility 0s linear 0s !important;
|
|
727
|
+
}
|
|
728
|
+
body.ui-bars-collapsed .slide-menu-top-bar {
|
|
729
|
+
transform: translateY(-100%);
|
|
730
|
+
}
|
|
731
|
+
body.ui-bars-collapsed .bottom-bar {
|
|
732
|
+
transform: translateY(100%) !important;
|
|
733
|
+
}
|
|
734
|
+
/* A bottom bar rendered inside the top bar leaves with it. */
|
|
735
|
+
body.ui-bars-collapsed .slide-menu-top-bar .bottom-bar {
|
|
736
|
+
transform: none !important;
|
|
737
|
+
}
|
|
738
|
+
body.ui-bars-collapsed .slide-menu-top-bar,
|
|
739
|
+
body.ui-bars-collapsed .bottom-bar {
|
|
740
|
+
visibility: hidden;
|
|
741
|
+
pointer-events: none;
|
|
742
|
+
transition:
|
|
743
|
+
transform 0.3s ease,
|
|
744
|
+
opacity 0.3s ease,
|
|
745
|
+
visibility 0s linear 0.3s !important;
|
|
746
|
+
}
|
|
747
|
+
body.ui-bars-animating .main-body,
|
|
748
|
+
body.ui-bars-animating .main-body-btn-container {
|
|
749
|
+
transition:
|
|
750
|
+
top 0.3s ease,
|
|
751
|
+
left 0.3s ease,
|
|
752
|
+
height 0.3s ease,
|
|
753
|
+
opacity 0.3s ease !important;
|
|
754
|
+
}
|
|
755
|
+
@media (prefers-reduced-motion: reduce) {
|
|
756
|
+
.slide-menu-top-bar,
|
|
757
|
+
.bottom-bar,
|
|
758
|
+
body.ui-bars-collapsed .slide-menu-top-bar,
|
|
759
|
+
body.ui-bars-collapsed .bottom-bar,
|
|
760
|
+
body.ui-bars-animating .main-body,
|
|
761
|
+
body.ui-bars-animating .main-body-btn-container {
|
|
762
|
+
transition: none !important;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
</style>
|
|
766
|
+
<div class="fix modal slide-menu-top-bar">
|
|
659
767
|
<div
|
|
660
768
|
class="fl top-bar ${options.barClass ? options.barClass : ''}"
|
|
661
769
|
style="height: ${originHeightTopBar}px;"
|
|
@@ -748,24 +856,28 @@ class Modal {
|
|
|
748
856
|
</div>`
|
|
749
857
|
: ''}
|
|
750
858
|
${idModal === 'modal-menu' && options.mode !== 'slide-menu-right'
|
|
751
|
-
? html
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
859
|
+
? html`${options.barMode === 'top-bottom-bar'
|
|
860
|
+
? ''
|
|
861
|
+
: html`<div
|
|
862
|
+
class="abs main-btn-menu-top-container"
|
|
863
|
+
style="bottom: 0px; left: 0px; z-index: 10; height: ${originHeightTopBar}px; width: ${originHeightTopBar}px"
|
|
864
|
+
>
|
|
865
|
+
${await BtnIcon.instance({
|
|
866
|
+
style: `height: 100%`,
|
|
867
|
+
class: `in fll main-btn-menu-top action-bar-box action-btn-center-top`,
|
|
868
|
+
label: html`<div class="abs center">
|
|
869
|
+
<i class="far fa-square btn-bar-center-icon-square hide"></i>
|
|
870
|
+
<span class="btn-bar-center-icon-close hide">${barConfig.buttons.close.label}</span>
|
|
871
|
+
<span class="btn-bar-center-icon-menu">${barConfig.buttons.menu.label}</span>
|
|
872
|
+
</div>`,
|
|
873
|
+
})}
|
|
874
|
+
</div>`}
|
|
765
875
|
|
|
766
876
|
<style>
|
|
877
|
+
/* Under 'top-bottom-bar' the left menu hamburger is the bottom bar's, so the
|
|
878
|
+
banner has no top bar button to clear. */
|
|
767
879
|
.a-link-top-banner {
|
|
768
|
-
padding-left:
|
|
880
|
+
padding-left: ${options.barMode === 'top-bottom-bar' ? 0 : 35}px;
|
|
769
881
|
}
|
|
770
882
|
.main-body-btn-bar-custom {
|
|
771
883
|
top: 50px !important;
|
|
@@ -789,7 +901,11 @@ class Modal {
|
|
|
789
901
|
}
|
|
790
902
|
s(`.main-btn-sign-up`).click();
|
|
791
903
|
});
|
|
792
|
-
if (
|
|
904
|
+
if (
|
|
905
|
+
idModal === 'modal-menu' &&
|
|
906
|
+
options.mode !== 'slide-menu-right' &&
|
|
907
|
+
options.barMode !== 'top-bottom-bar'
|
|
908
|
+
) {
|
|
793
909
|
EventsUI.onClick(`.action-btn-center-top`, (e) => {
|
|
794
910
|
e.preventDefault();
|
|
795
911
|
Modal.actionBtnCenter();
|
|
@@ -1160,6 +1276,7 @@ class Modal {
|
|
|
1160
1276
|
} catch (e) {}
|
|
1161
1277
|
}
|
|
1162
1278
|
Modal.removeModal(searchBoxHistoryId);
|
|
1279
|
+
updateBarCustomVisibility();
|
|
1163
1280
|
};
|
|
1164
1281
|
s('.top-bar-search-box').onblur = () => {
|
|
1165
1282
|
hoverFocusCtl.checkDismiss();
|
|
@@ -1385,10 +1502,15 @@ class Modal {
|
|
|
1385
1502
|
>
|
|
1386
1503
|
${await BtnIcon.instance({
|
|
1387
1504
|
style: `height: 100%`,
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1505
|
+
// The left menu hamburger under 'top-bottom-bar'. Otherwise a left menu is
|
|
1506
|
+
// toggled from the top bar and a right menu from the floating
|
|
1507
|
+
// .main-body-btn-menu, so a copy here would only duplicate one of them.
|
|
1508
|
+
class: `in fl${options.mode === 'slide-menu-right' ? 'r' : 'l'} main-btn-menu action-bar-box action-btn-center ${
|
|
1509
|
+
options?.disableTools?.includes('center') ||
|
|
1510
|
+
options.mode === 'slide-menu-right' ||
|
|
1511
|
+
options.barMode !== 'top-bottom-bar'
|
|
1512
|
+
? 'hide'
|
|
1513
|
+
: ''
|
|
1392
1514
|
}`,
|
|
1393
1515
|
label: html`
|
|
1394
1516
|
<div class="${contentIconClass}">
|
|
@@ -1662,6 +1784,8 @@ class Modal {
|
|
|
1662
1784
|
removeEvent();
|
|
1663
1785
|
}
|
|
1664
1786
|
});
|
|
1787
|
+
Scroll.setEvent('.main-body', () => updateBarCustomVisibility());
|
|
1788
|
+
updateBarCustomVisibility();
|
|
1665
1789
|
setTimeout(window.onresize);
|
|
1666
1790
|
setRouterReady();
|
|
1667
1791
|
});
|
|
@@ -227,7 +227,9 @@ class AppShellUnderpost {
|
|
|
227
227
|
titleRender: () => {
|
|
228
228
|
return '';
|
|
229
229
|
},
|
|
230
|
-
mode: 'slide-menu-right',
|
|
230
|
+
// mode: 'slide-menu-right',
|
|
231
|
+
barMode: 'top-bottom-bar',
|
|
232
|
+
mode: 'slide-menu',
|
|
231
233
|
RouterInstance,
|
|
232
234
|
searchCustomImgClass: 'underpost-menu-icon',
|
|
233
235
|
htmlMainBody: async () => {
|
|
@@ -305,6 +307,16 @@ class AppShellUnderpost {
|
|
|
305
307
|
return panelFormInstance;
|
|
306
308
|
},
|
|
307
309
|
});
|
|
310
|
+
for (const eventFn of ['onCollapseMenuListener', 'onExtendMenuListener', 'onExpandUiListener', 'onCloseListener']) {
|
|
311
|
+
Modal.Data['modal-menu'][eventFn]['underpost-panel-form-padding'] = (arg) => {
|
|
312
|
+
if (s(`.underpost-panel-form-container`))
|
|
313
|
+
s(`.underpost-panel-form-container`).style.paddingLeft =
|
|
314
|
+
!s('.main-body-btn-ui-open').classList.contains('hide') &&
|
|
315
|
+
s(`.btn-bar-center-icon-close`).classList.contains('hide')
|
|
316
|
+
? '50px'
|
|
317
|
+
: '0px';
|
|
318
|
+
};
|
|
319
|
+
}
|
|
308
320
|
|
|
309
321
|
ThemeEvents['underpost-main-theme-event'] = () => {
|
|
310
322
|
const srcLogo = darkTheme
|