@bluestep-systems/bspecs 0.10.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 +129 -0
- package/cli.js +74 -0
- package/package.json +30 -0
- package/src/prompts.js +74 -0
- package/src/scaffold.js +152 -0
- package/src/sync.js +123 -0
- package/src/utils.js +95 -0
- package/templates/claude/agents/b6p-code-review.md +81 -0
- package/templates/claude/agents/b6p-commenter.md +59 -0
- package/templates/claude/agents/b6p-task-implementer.md +77 -0
- package/templates/claude/hooks/block-generated-files.sh +16 -0
- package/templates/claude/hooks/block-tsc.sh +16 -0
- package/templates/claude/hooks/prettier-on-save.sh +21 -0
- package/templates/claude/instructions/b6p-platform.md.template +185 -0
- package/templates/claude/instructions/bsjs-development.md.template +430 -0
- package/templates/claude/instructions/conventions/always-snapshot.md.template +25 -0
- package/templates/claude/instructions/conventions/blueiq-no-ai-branding.md.template +11 -0
- package/templates/claude/instructions/conventions/date-format.md.template +27 -0
- package/templates/claude/instructions/conventions/endpoint-approach.md.template +9 -0
- package/templates/claude/instructions/conventions/formula-patterns.md.template +71 -0
- package/templates/claude/instructions/conventions/no-global-dollar.md.template +9 -0
- package/templates/claude/instructions/conventions/push-inner-draft.md.template +21 -0
- package/templates/claude/instructions/conventions/separate-files.md.template +17 -0
- package/templates/claude/instructions/conventions/single-script.md.template +28 -0
- package/templates/claude/instructions/conventions/snapshot-integrity.md.template +23 -0
- package/templates/claude/instructions/conventions/top-level-const-tdz.md.template +33 -0
- package/templates/claude/instructions/conventions/ts-in-template-literal.md.template +48 -0
- package/templates/claude/instructions/conventions/tsc-rootdir.md.template +17 -0
- package/templates/claude/instructions/gotchas/common-gotchas.md.template +91 -0
- package/templates/claude/instructions/gotchas/fetched-resource-code.md.template +9 -0
- package/templates/claude/instructions/index.md.template +82 -0
- package/templates/claude/instructions/reference/api-patterns.md.template +487 -0
- package/templates/claude/instructions/reference/blueiq-credit-integration-playbook.md.template +31 -0
- package/templates/claude/instructions/reference/chronounit-months.md.template +37 -0
- package/templates/claude/instructions/reference/code-patterns.md.template +265 -0
- package/templates/claude/instructions/reference/component-library.md.template +217 -0
- package/templates/claude/instructions/reference/crm-dashboard-inspo.md.template +17 -0
- package/templates/claude/instructions/reference/csv-parsing.md.template +18 -0
- package/templates/claude/instructions/reference/dashboard-design-system.md.template +38 -0
- package/templates/claude/instructions/reference/datetime-field-write.md.template +27 -0
- package/templates/claude/instructions/reference/design-system.md.template +150 -0
- package/templates/claude/instructions/reference/dpn-dashboard-framework.md.template +29 -0
- package/templates/claude/instructions/reference/endpoint-method-call.md.template +10 -0
- package/templates/claude/instructions/reference/endpoint-no-delete-method.md.template +9 -0
- package/templates/claude/instructions/reference/endpoint-output-channel.md.template +23 -0
- package/templates/claude/instructions/reference/endpoint-urls.md.template +15 -0
- package/templates/claude/instructions/reference/entry-delete.md.template +40 -0
- package/templates/claude/instructions/reference/file-execution.md.template +113 -0
- package/templates/claude/instructions/reference/http-requester.md.template +37 -0
- package/templates/claude/instructions/reference/id-full-vs-short.md.template +15 -0
- package/templates/claude/instructions/reference/internal-loopback-fetch.md.template +24 -0
- package/templates/claude/instructions/reference/localdate-parse.md.template +16 -0
- package/templates/claude/instructions/reference/merge-report-memo-json.md.template +25 -0
- package/templates/claude/instructions/reference/merge-report-static-index.md.template +29 -0
- package/templates/claude/instructions/reference/merge-report-urls.md.template +67 -0
- package/templates/claude/instructions/reference/multi-entry-in-multi-entry.md.template +21 -0
- package/templates/claude/instructions/reference/named-controls-submit.md.template +11 -0
- package/templates/claude/instructions/reference/new-entry-id.md.template +30 -0
- package/templates/claude/instructions/reference/relationship-field-set.md.template +37 -0
- package/templates/claude/instructions/reference/send-message-abort.md.template +37 -0
- package/templates/claude/instructions/reference/session-cookie-forwarding.md.template +31 -0
- package/templates/claude/instructions/reference/singleselect-null-copy.md.template +21 -0
- package/templates/claude/instructions/reference/staff-query-permission-gating.md.template +27 -0
- package/templates/claude/instructions/reference/timefield-vs-datetimefield.md.template +13 -0
- package/templates/claude/instructions/reference/user-zone-id.md.template +16 -0
- package/templates/claude/settings.json.template +46 -0
- package/templates/claude/skills/b6p-audit/SKILL.md +82 -0
- package/templates/claude/skills/b6p-pull/SKILL.md +123 -0
- package/templates/claude/skills/b6p-push/SKILL.md +70 -0
- package/templates/claude/skills/bug-fix/SKILL.md +28 -0
- package/templates/claude/skills/spec-create/SKILL.md +60 -0
- package/templates/claude/skills/spec-execute/SKILL.md +51 -0
- package/templates/claude/skills/spec-status/SKILL.md +20 -0
- package/templates/claude/skills/task-comment/SKILL.md +96 -0
- package/templates/claude/spec-templates/design.template.md +36 -0
- package/templates/claude/spec-templates/requirements.template.md +26 -0
- package/templates/claude/spec-templates/tasks.template.md +37 -0
- package/templates/module/README.md.template +46 -0
- package/templates/root/.gitignore.template +14 -0
- package/templates/root/.prettierrc.template +8 -0
- package/templates/root/CLAUDE.md.template +157 -0
- package/templates/root/README.md.template +58 -0
- package/templates/root/package.json.template +15 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# Code Patterns
|
|
2
|
+
|
|
3
|
+
Common code patterns and examples for BlueStep.js development.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
- [Query Patterns](#query-patterns)
|
|
8
|
+
- [Merge Report Patterns](#merge-report-patterns)
|
|
9
|
+
- [Endpoint Patterns](#endpoint-patterns)
|
|
10
|
+
- [Error Handling Patterns](#error-handling-patterns)
|
|
11
|
+
- [Performance Patterns](#performance-patterns)
|
|
12
|
+
- [Component Import Pattern](#component-import-pattern)
|
|
13
|
+
- [Debugging Patterns](#debugging-patterns)
|
|
14
|
+
|
|
15
|
+
## Query Patterns
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
// Simple query
|
|
19
|
+
B.queries.byFID['staffQuery'].query().forEach((record: Bluestep.Relate.Record) => {
|
|
20
|
+
console.log(record.forms.nameForm.fields.firstName.opt().orElse(''));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Query with filtering (Java collections have no .filter()/.map())
|
|
24
|
+
const results = [];
|
|
25
|
+
B.queries.byFID['staffQuery'].query().forEach((record: Bluestep.Relate.Record) => {
|
|
26
|
+
const email = record.forms.contact.fields.email.opt().orElse('');
|
|
27
|
+
if (email.endsWith('@company.com')) results.push(record);
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The examples above run an ad-hoc query (read-only). For configured queries exposed as directly-iterable top-level variables (via the platform form-import config) and writable-context rules, see [api-patterns](api-patterns.md#query-access-patterns).
|
|
32
|
+
|
|
33
|
+
## Merge Report Patterns
|
|
34
|
+
|
|
35
|
+
Server code (`scripts/app.ts`) can access `B`; client code (`static/script.js`) runs in the browser and cannot. Bridge the two with `window` variables.
|
|
36
|
+
|
|
37
|
+
### Server-side setup
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// scripts/app.ts - renders the page container
|
|
41
|
+
const dashboardId = 'dashboard_123';
|
|
42
|
+
const isSuper = B.optUser.map(u => u.isGlobalSuper()).orElse(false);
|
|
43
|
+
|
|
44
|
+
const vars = `
|
|
45
|
+
<script>
|
|
46
|
+
window.dashboardId = '${dashboardId}';
|
|
47
|
+
window.isSuper = ${isSuper};
|
|
48
|
+
window.endpointUrl = '/b/apiEndpoint';
|
|
49
|
+
</script>
|
|
50
|
+
`;
|
|
51
|
+
B.net.pageContent('vars').HEADER_SCRIPTS_BOTTOM().addContent(vars).insert();
|
|
52
|
+
|
|
53
|
+
B.out = '<div id="app-container">Loading...</div>';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### `B.net.pageContent()` placements
|
|
57
|
+
|
|
58
|
+
`B.net.pageContent(lookup)` injects content into a specific location of the page. Chain one placement method, then `.addContent(html)`, then `.insert()`.
|
|
59
|
+
|
|
60
|
+
- `.HEADER_SCRIPTS_BOTTOM()` — bottom of the scripts section in `<head>` (most common for variables)
|
|
61
|
+
- `.HEADER_SCRIPTS_TOP()` — top of the scripts section in `<head>`
|
|
62
|
+
- `.HEADER_CSS_BOTTOM()` / `.HEADER_CSS_TOP()` — CSS section in `<head>`
|
|
63
|
+
- `.HEADER_TOP()` / `.HEADER_BOTTOM()` — top/bottom of `<head>`
|
|
64
|
+
- `.PAGE_END()` — near the bottom of the page (default)
|
|
65
|
+
|
|
66
|
+
### Passing per-row data (queue / lazy-init pattern)
|
|
67
|
+
|
|
68
|
+
When a merge-report column renders per-row data, use a queue + lazy-init pattern rather than a `window` global. This avoids a timing race where `DOMContentLoaded` fires before the inline `<script>` from `B.out` has executed.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// scripts/app.ts - push the value and trigger init if ready
|
|
72
|
+
const clientId = name.fields.sysId.val();
|
|
73
|
+
B.out = `<script>
|
|
74
|
+
(window._myPending = window._myPending || []).push("${clientId}");
|
|
75
|
+
if (window._myInit) window._myInit();
|
|
76
|
+
</script>`;
|
|
77
|
+
|
|
78
|
+
// static/script.ts - define _myInit and self-call at load time (no DOMContentLoaded)
|
|
79
|
+
function initWidget(clientId: string): void {
|
|
80
|
+
const root = document.getElementById('my-root') as HTMLElement | null;
|
|
81
|
+
if (!root) return;
|
|
82
|
+
// ... render using clientId
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
(window as any)._myInit = function(): void {
|
|
86
|
+
const pending: string[] = (window as any)._myPending || [];
|
|
87
|
+
(window as any)._myPending = [];
|
|
88
|
+
while (pending.length) initWidget(pending.shift()!);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
(window as any)._myInit(); // drain anything queued before this script loaded
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Use a **unique namespace per report** (e.g. `_dpnBreakdownPending`) so multiple reports on the same page don't collide.
|
|
95
|
+
|
|
96
|
+
### Client-side initialization
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// static/script.js - main application logic
|
|
100
|
+
(async function() {
|
|
101
|
+
const dashboardId = window.dashboardId;
|
|
102
|
+
const endpointUrl = window.endpointUrl;
|
|
103
|
+
try {
|
|
104
|
+
const response = await fetch(endpointUrl, {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
107
|
+
body: JSON.stringify({ action: 'loadDashboard', dashboardId })
|
|
108
|
+
});
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
if (data.success) initializeApp(data.config);
|
|
111
|
+
else console.error('Failed to load:', data.error);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error('Error initializing:', error);
|
|
114
|
+
}
|
|
115
|
+
})();
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Endpoint Patterns
|
|
119
|
+
|
|
120
|
+
Endpoints receive the HTTP request via `B.net.request` and respond via `B.net.response`.
|
|
121
|
+
|
|
122
|
+
### Request handling
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
const { request } = B.net;
|
|
126
|
+
const id = request.optParameter('id').orElse(''); // Java Optional
|
|
127
|
+
const name = request.parameter('name'); // string or null
|
|
128
|
+
const bodyStr = request.content(); // request body as string (JSON POST)
|
|
129
|
+
const body = JSON.parse(bodyStr);
|
|
130
|
+
const method = request.method(); // "GET", "POST", ...
|
|
131
|
+
const contentType = request.optHeader('Content-Type').orElse('');
|
|
132
|
+
const path = request.path(); // e.g. "/b/myEndpoint"
|
|
133
|
+
const query = request.queryString(); // e.g. "id=123&page=2"
|
|
134
|
+
const fullUrl = request.fullUrl();
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Response handling
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const { response } = B.net;
|
|
141
|
+
response.out(JSON.stringify({ success: true, data: results })); // most common
|
|
142
|
+
response.contentType('application/json');
|
|
143
|
+
response.status(200);
|
|
144
|
+
response.header('Cache-Control', 'no-cache');
|
|
145
|
+
response.sendRedirect('/b/otherEndpoint');
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Action-based router
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// scripts/app.ts
|
|
152
|
+
try {
|
|
153
|
+
const { request, response } = B.net;
|
|
154
|
+
const action = request.optParameter('action').orElse('');
|
|
155
|
+
|
|
156
|
+
let body: any = {};
|
|
157
|
+
try { body = JSON.parse(request.content()); } catch (e) { /* not JSON, use parameters */ }
|
|
158
|
+
|
|
159
|
+
switch (action) {
|
|
160
|
+
case 'getData': {
|
|
161
|
+
const queryFid = body.queryFid || request.optParameter('queryFid').orElse('');
|
|
162
|
+
response.out(JSON.stringify({ success: true, data: getQueryData(queryFid) }));
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'updateRecord': {
|
|
166
|
+
updateRecord(body.recordId, body.updates);
|
|
167
|
+
response.out(JSON.stringify({ success: true }));
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
default:
|
|
171
|
+
response.out(JSON.stringify({ success: false, error: `Unknown action: ${action}` }));
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error('Error in endpoint:', error);
|
|
175
|
+
B.net.response.out(JSON.stringify({ success: false, error: error.message || String(error) }));
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Error Handling Patterns
|
|
180
|
+
|
|
181
|
+
### Try-catch with user feedback (client-side)
|
|
182
|
+
|
|
183
|
+
SweetAlert2 v8.18.4 API (`type:`, `result.value`) — not v11. See [common-gotchas](../gotchas/common-gotchas.md#sweetalert2-version-differences).
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
async function performAction() {
|
|
187
|
+
try {
|
|
188
|
+
const result = await apiCall();
|
|
189
|
+
if (result.success) {
|
|
190
|
+
Swal.fire({ type: 'success', title: 'Success', text: 'Operation completed successfully' });
|
|
191
|
+
} else {
|
|
192
|
+
throw new Error(result.error || 'Operation failed');
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error performing action:', error);
|
|
196
|
+
Swal.fire({ type: 'error', title: 'Error', text: error.message || 'An error occurred' });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Validation pattern
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
function validateInput(data: any): { valid: boolean, error?: string } {
|
|
205
|
+
if (!data.name || data.name.trim() === '') return { valid: false, error: 'Name is required' };
|
|
206
|
+
if (!data.email || !data.email.includes('@')) return { valid: false, error: 'Valid email is required' };
|
|
207
|
+
return { valid: true };
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Performance Patterns
|
|
212
|
+
|
|
213
|
+
### Caching query results (client-side)
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
const dataCache = new Map();
|
|
217
|
+
|
|
218
|
+
async function getQueryData(queryFid: string, forceRefresh = false) {
|
|
219
|
+
if (!forceRefresh && dataCache.has(queryFid)) return dataCache.get(queryFid);
|
|
220
|
+
const response = await fetch(endpointUrl, {
|
|
221
|
+
method: 'POST',
|
|
222
|
+
body: JSON.stringify({ action: 'getQueryData', queryFid })
|
|
223
|
+
});
|
|
224
|
+
const result = await response.json();
|
|
225
|
+
if (result.success) { dataCache.set(queryFid, result.data); return result.data; }
|
|
226
|
+
throw new Error(result.error || 'Failed to load data');
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Batch processing
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
function processBatch(records: any[], batchSize = 100) {
|
|
234
|
+
const results = [];
|
|
235
|
+
for (let i = 0; i < records.length; i += batchSize) {
|
|
236
|
+
const batch = records.slice(i, i + batchSize);
|
|
237
|
+
batch.forEach(record => results.push(processRecord(record)));
|
|
238
|
+
}
|
|
239
|
+
return results;
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Component Import Pattern
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// objects/helpers.ts
|
|
247
|
+
export function formatDate(date: any): string { /* ... */ return formatted; }
|
|
248
|
+
export function formatCurrency(amount: number): string { /* ... */ return formatted; }
|
|
249
|
+
|
|
250
|
+
// scripts/app.ts
|
|
251
|
+
import { formatDate, formatCurrency } from './objects/helpers';
|
|
252
|
+
const formatted = formatDate(dateValue);
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
A file only executes if it is imported — see [file-execution](file-execution.md).
|
|
256
|
+
|
|
257
|
+
## Debugging Patterns
|
|
258
|
+
|
|
259
|
+
```typescript
|
|
260
|
+
// At top of file
|
|
261
|
+
const DEBUG = false; // set to true for verbose logging
|
|
262
|
+
|
|
263
|
+
if (DEBUG) console.log('Processing record:', recordId);
|
|
264
|
+
console.error('Failed to load data:', error); // always log errors
|
|
265
|
+
```
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# Component Library
|
|
2
|
+
|
|
3
|
+
> **Note**: for the complete component API, see the JSDoc in `https://files.bluestep.net/script/530024___41/static/genericComponents.js`. This file contains usage patterns and best practices.
|
|
4
|
+
>
|
|
5
|
+
> **Design System**: for visual examples, color palette, typography, and spacing, see [design-system](design-system.md) or the [interactive design system merge report](https://bluestepplatformsupport.bluestep.net/shared/layouts/singleblock.jsp?_event=view&_id=120130___195147).
|
|
6
|
+
|
|
7
|
+
**Always check this library before writing custom HTML-generation code.**
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
- [Component Source Priority](#component-source-priority)
|
|
12
|
+
- [Configuration and Importing](#configuration-and-importing)
|
|
13
|
+
- [CSS and Merge Report Styling](#css-and-merge-report-styling)
|
|
14
|
+
- [Design Standards](#design-standards)
|
|
15
|
+
- [Common Components](#common-components)
|
|
16
|
+
- [Best Practices](#best-practices)
|
|
17
|
+
- [When NOT to Use Components](#when-not-to-use-components)
|
|
18
|
+
|
|
19
|
+
## Component Source Priority
|
|
20
|
+
|
|
21
|
+
When building UI, source components in this order:
|
|
22
|
+
|
|
23
|
+
1. **BlueStep generic components** — use `moduleF`, `headF`, `svgF`, `tableF`, etc. first.
|
|
24
|
+
2. **Bootstrap 3.3.7 components** — standard Bootstrap (buttons, alerts, modals, panels) when no generic component exists.
|
|
25
|
+
3. **Custom components** — only when necessary, matching the styles of the above.
|
|
26
|
+
|
|
27
|
+
## Configuration and Importing
|
|
28
|
+
|
|
29
|
+
The recommended setup is to register the library in `info/config.json`, then import with the short name:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"models": [
|
|
34
|
+
{
|
|
35
|
+
"name": "genericComponents.ts",
|
|
36
|
+
"url": "https://files.bluestep.net/script/530024___41/static/genericComponents.ts"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// ✅ Recommended - when configured in config.json
|
|
44
|
+
import { moduleF, svgF, headF, tableF } from 'genericComponents';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
If it is not configured, you can import from the full hosted URL as a fallback, but adding it to `config.json` is preferred:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { moduleF, svgF, headF, tableF } from 'https://files.bluestep.net/script/530024___41/static/genericComponents.js';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
> ⚠️ **`type="module"` required in `index.html`.** When `script.ts` uses an ES `import` (including importing from `genericComponents`), the `<script>` tag must include `type="module"`, or the browser throws `SyntaxError: Cannot use import statement outside a module` and the script silently fails.
|
|
54
|
+
>
|
|
55
|
+
> ```html
|
|
56
|
+
> <script type="module" src=".build/script.js"></script>
|
|
57
|
+
> ```
|
|
58
|
+
|
|
59
|
+
## CSS and Merge Report Styling
|
|
60
|
+
|
|
61
|
+
Standard BlueStep pages (Connect/Manage) already provide their own HTML and CSS, and the component-library functions (`moduleF`, `headF`, `svgF`, `tableF`, `fieldF`, `bootstrapFormF`) are designed to work within that styling.
|
|
62
|
+
|
|
63
|
+
- **Do NOT add CSS in the merge report for component-library output.** It is already styled by the platform and the library; extra CSS is unnecessary and can conflict with the theme and layout.
|
|
64
|
+
- **Add CSS only for custom HTML** you introduce outside the standard components (custom wrappers, one-off layouts, third-party widgets). Put it in `static/styles.css` referenced from `static/index.html`, scoped to that custom markup only.
|
|
65
|
+
|
|
66
|
+
## Design Standards
|
|
67
|
+
|
|
68
|
+
### Color palette (CSS variables)
|
|
69
|
+
|
|
70
|
+
Always use CSS variables for colors so they stay consistent across themes.
|
|
71
|
+
|
|
72
|
+
Standard colors (available on all BlueStep pages):
|
|
73
|
+
|
|
74
|
+
- `--bs-red: #c03b2b` | `--bs-orange: #F29D1F` | `--bs-yellow: #EFC319`
|
|
75
|
+
- `--bs-green: #28AE60` | `--bs-blue: #2A81BA` | `--bs-purple: #894C9E`
|
|
76
|
+
- `--bs-logo-blue: #0063A6` (brand logo color, used in email templates)
|
|
77
|
+
- `--bs-gray: #969FA0` | `--bs-default: #2D3F50` (default theme color)
|
|
78
|
+
|
|
79
|
+
Custom generated colors (vary by the organization's color style):
|
|
80
|
+
|
|
81
|
+
- `--bs-primaryColor` | `--bs-primaryColorHigh`
|
|
82
|
+
- `--bs-accent1Color` | `--bs-accent2Color`
|
|
83
|
+
- `--bs-secondaryColor` | `--bs-secondaryColorHigh`
|
|
84
|
+
|
|
85
|
+
### Available components
|
|
86
|
+
|
|
87
|
+
- **SVG Icons** (`svgF` / `svgIconF`, `svgTitleF`) | **Headers** (`headF`) | **Tables** (`tableF`)
|
|
88
|
+
- **Fields** (`fieldF`) | **Bootstrap Forms** (`bootstrapFormF`) | **Modules** (`moduleF`)
|
|
89
|
+
- **Generic Tags** (`tagF`) | **Anchors** (`aF`) | **Email Templates** (`emailF`)
|
|
90
|
+
- **Breadcrumbs** (`breadcrumbF`) | **Search** (`searchHeadF`, `searchTextF`)
|
|
91
|
+
- **Date Input** (`bsDateInput`) | **Date Range** (`jsDateRange`) | **Popin Hints** (`popinHintF`)
|
|
92
|
+
- **Horizontal Forms** (`bsHorizFormCol2F`, `bsHorizFieldCol2F`)
|
|
93
|
+
|
|
94
|
+
## Common Components
|
|
95
|
+
|
|
96
|
+
### SVG Icons
|
|
97
|
+
|
|
98
|
+
**When to use**: all icon needs (don't use `<img>` directly).
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
import { svgF } from 'genericComponents';
|
|
102
|
+
|
|
103
|
+
const icon = svgF({ icon: 'settings', size: 48 });
|
|
104
|
+
const colored = svgF({ icon: 'settings', size: 24, color: 'white blue' });
|
|
105
|
+
// Common icons: trash, pencil, gear3, circleCheck, circleX, calendar2, magnify, plus
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
⚠️ **REQUIRED: reference the icon endpoint before using any icon.** Before selecting or suggesting an icon, query the official BlueStep SVG icons endpoint:
|
|
109
|
+
|
|
110
|
+
- **URL**: `https://bluestepplatformsupport.bluestep.net/b/svgIconsList`
|
|
111
|
+
- **Returns**: a JSON array where each icon has `icoName` (e.g. `"calendar.svg"`), `category` (e.g. `["Standard", "System and Technical"]`), and `icoKeywords` (e.g. `"calendar, day, week, month, year, schedule"`).
|
|
112
|
+
|
|
113
|
+
How to use it: search the endpoint by name, category, or keyword; use the icon name **without** the `.svg` extension (`calendar.svg` → `svgF({ icon: 'calendar', size: 24 })`); match the icon to context via categories/keywords. Never guess icon names or use icons not listed in the endpoint.
|
|
114
|
+
|
|
115
|
+
### Headers
|
|
116
|
+
|
|
117
|
+
**When to use**: section headers, collapsible sections.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { headF } from 'genericComponents';
|
|
121
|
+
|
|
122
|
+
const header = headF({ title: 'Section Title' });
|
|
123
|
+
const withButton = headF({ title: 'Section Title', button: '<button>Action</button>', hint: 'Help text' });
|
|
124
|
+
const collapsible = headF({ title: 'Collapsible Section', isCollapsable: true, content: '<div>Content</div>', isOpen: true });
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Tables
|
|
128
|
+
|
|
129
|
+
**When to use**: simple data tables (use Tabulator for complex tables).
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { tableF } from 'genericComponents';
|
|
133
|
+
|
|
134
|
+
const table = tableF({
|
|
135
|
+
className: 'table table-striped',
|
|
136
|
+
showHeaders: true,
|
|
137
|
+
tableRows: [ { 'Name': 'John', 'Age': '30' }, { 'Name': 'Jane', 'Age': '25' } ]
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Modules
|
|
142
|
+
|
|
143
|
+
**When to use**: full page layouts in Connect/Manage.
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { moduleF } from 'genericComponents';
|
|
147
|
+
|
|
148
|
+
const module = moduleF({
|
|
149
|
+
id: 'dashboard',
|
|
150
|
+
header: 'Dashboard',
|
|
151
|
+
tabs: [
|
|
152
|
+
{ icon: 'home', label: 'Overview', className: 'cDefault', tabContent: overviewContent },
|
|
153
|
+
{ icon: 'chart', label: 'Reports', className: 'cReports', tabContent: reportsContent }
|
|
154
|
+
]
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**Module colors**: cDefault, cReports, cOrange, cGreen, cBlue, cYellow, cPurple, cRed, cBrown.
|
|
159
|
+
|
|
160
|
+
### Email Templates
|
|
161
|
+
|
|
162
|
+
**When to use**: all email generation (don't create custom email HTML).
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
import { emailF } from 'genericComponents';
|
|
166
|
+
|
|
167
|
+
const email = emailF({
|
|
168
|
+
title: 'Account Verification',
|
|
169
|
+
previewText: 'Please verify your email address',
|
|
170
|
+
bodyText: 'Click the button below to verify your email address.',
|
|
171
|
+
btnLink: 'https://example.com/verify?token=xyz',
|
|
172
|
+
btnText: 'Verify Email',
|
|
173
|
+
primaryColor: '0063A6',
|
|
174
|
+
logoImg: 'https://example.com/logo.png'
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Forms
|
|
179
|
+
|
|
180
|
+
**When to use**: form inputs in Connect/Manage pages.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
import { bootstrapFormF } from 'genericComponents';
|
|
184
|
+
|
|
185
|
+
const form = bootstrapFormF({
|
|
186
|
+
id: 'user-form',
|
|
187
|
+
labelSize: 'third',
|
|
188
|
+
fields: [
|
|
189
|
+
{ fieldLabel: 'Name', fieldId: 'name', fieldType: 'text' },
|
|
190
|
+
{ fieldLabel: 'Role', fieldId: 'role', fieldType: 'select',
|
|
191
|
+
fieldList: [ { idValue: 'admin', label: 'Administrator' }, { idValue: 'user', label: 'User' } ] },
|
|
192
|
+
{ fieldLabel: 'Active', fieldId: 'active', fieldType: 'checkbox', fieldIsSelected: true }
|
|
193
|
+
]
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
(For generating form fields from real BlueStep fields with labels and validation, see `mergeTag()` in [api-patterns](api-patterns.md#mergetag-method).)
|
|
198
|
+
|
|
199
|
+
## Best Practices
|
|
200
|
+
|
|
201
|
+
**DO ✅**
|
|
202
|
+
|
|
203
|
+
1. Use the component library first; reach for raw HTML only when nothing fits.
|
|
204
|
+
2. Use semantic components: `moduleF()` for layouts, `headF()` for headers, `svgF()` for icons, `emailF()` for emails.
|
|
205
|
+
3. Check the JSDoc in the source for the complete API.
|
|
206
|
+
|
|
207
|
+
**DON'T ❌**
|
|
208
|
+
|
|
209
|
+
1. **Don't add CSS for component-library output** — it is already styled (see [CSS and Merge Report Styling](#css-and-merge-report-styling)).
|
|
210
|
+
2. **Don't recreate existing components** — use `tableF({ tableRows: data })`, not a hand-built `<table>`.
|
|
211
|
+
3. **Don't use inline SVG or `<img>` for icons** — use `svgF({ icon: 'settings', size: 24 })`.
|
|
212
|
+
4. **Don't create custom email HTML** — always use `emailF()`.
|
|
213
|
+
5. **Don't mix component styles** — use the Bootstrap classes from the components and the module color scheme.
|
|
214
|
+
|
|
215
|
+
## When NOT to Use Components
|
|
216
|
+
|
|
217
|
+
Some cases require custom HTML: complex interactive UIs (use frameworks like Tabulator), real-time dashboards with heavy client logic, or highly customized layouts that don't match BlueStep patterns. Even then, use components for sub-elements (`svgF`, `headF`, etc.).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "CRM Intelligence Dashboard (beh/1433876) — 12-page sales SPA, reference/inspiration for the Behavioral Team Scorecard deal metrics"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
**CRM Intelligence Dashboard** — BlueStep merge report at **beh.bluestep.net/files/1433876** (script "CRM Dashboard", key 530024___82). A CEO/CRO-grade sales-pipeline SPA with 12 interactive pages. Pulled as inspiration for behavioral scorecard deal-based metrics. NOT to be modified — reference only.
|
|
6
|
+
|
|
7
|
+
**Architecture (the pattern to reuse):**
|
|
8
|
+
- `scripts/app.ts` (server, ~287 lines): iterates `allPrograms` ONCE, flattens each program + its `deals` and `followups` sub-forms into plain `ProgramLite` / `DealLite` / `FollowupLite` objects, builds a `{meta, programs, deals, followups}` payload, and emits it as `<script id="crm-data" type="application/json">…</script>` + an empty `<div id="crm-app">`. JSON is escaped with `.replace(/</g, '\\u003c')` before embedding.
|
|
9
|
+
- `static/script.ts` (client, ~3700 lines): reads the JSON payload, renders the SPA — global filter bar + tab dispatch (`renderActiveTab`) + 12 page renderers (`renderOverview`, `renderRevenue`, `renderPipeline`, `renderSources`, `renderOwners`, `renderProducts`, `renderWinLoss`, `renderVelocity`, `renderFollowups`, `renderTrends`, `renderConferences`, `renderExplorer`) + a shared deal-detail modal.
|
|
10
|
+
- **Single-file client bundle**: BlueStep compiles ONLY `static/script.ts` → `.build/script.js`. The `static/util/*.ts`, `static/pages/*.ts`, `static/filters.ts`, `static/tabs.ts`, `static/types.ts` files are source-of-record copies, NOT compiled — all live code is inlined into script.ts. (Confirms [single script](../conventions/single-script.md).)
|
|
11
|
+
- `static/index.html`: loads Chart.js 4 from jsDelivr CDN, then `.build/script.js`, then `styles.css`. Charts use Chart.js.
|
|
12
|
+
|
|
13
|
+
**Reusable techniques:** canonical-vs-observed dropdown merging (`mergeUnique`); per-entity try/catch so one bad deal/program doesn't kill the report; weighted revenue forecast by confidence color (Green/Yellow/Red); HTML funnel, donuts, stacked bars, heatmaps, age histograms; CSV export; deal modal keyed by id.
|
|
14
|
+
|
|
15
|
+
**Server field-read helpers worth copying:** `formatDate(field)` (DateField→LocalDate.format, falls back to LocalDateTime.toLocalDate), `readSingleSelect(field)` (`field.opt().map(v=>v.displayName()).orElse('')`), multi-select via `field.selectedNames().toArray()`.
|
|
16
|
+
|
|
17
|
+
**CAUTION when reusing on the scorecard:** the CRM dashboard reads fields from ITS OWN context (e.g. `info.ownerTxt`, `info.city/state`, `deal.dealOwnerTxt` text mirror, `followups` form with `dealRel`/`ownerRel`/`completeSig`). The scorecard's `deals` form (its own declarations) uses `dealOwnerRel` (relationship, not Txt) and may not expose the same `info`/`followups` fields. Always build scorecard deal metrics against the scorecard's `1434276/declarations/index.d.ts`, not by assuming CRM field names.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: BlueStep merge reports CAN read & parse uploaded CSVs — built-in B.csv() parser + DocumentLinkField/Document/fetch access
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
A server-side BSJS merge report CAN access and parse an uploaded CSV at runtime. BlueStep ships a first-class CSV parser — no hand-rolled splitting needed.
|
|
6
|
+
|
|
7
|
+
**Parser:** `B.csv(input, charset?)` (aliases `B.io.csv` / `B.text.csv`). Accepts a Reader, InputStream, FetchedResource, or string. Methods: `.toListOfObjects()` (array of objects keyed by header row — easiest), `.forEach((rowEList, i)=>…)` (streaming), `.toList()` (2D), and chainable `.fieldDelimeter()/.textDelimeter()/.escapeCharacter()`. Rows from `.row()` are Java-backed `EList<string>` — use `.forEach`/index, NOT JS `.map()`; `toListOfObjects()` rows behave like plain objects.
|
|
8
|
+
|
|
9
|
+
**Three ways to get the file's bytes (priority order):**
|
|
10
|
+
1. **DocumentLinkField on a form/record (preferred)** — an uploaded-file field. `field.content()` → string; `field.forReader(r => B.csv(r).toListOfObjects())` streams it. Also `.forInputStream()`, `.toBytes()`, `.filename()`, `.contentType()`, `.permUrl()/.davUrl()`. No HTTP hop, no auth ambiguity.
|
|
11
|
+
2. **Document in a folder** — `folder.documents()['name.csv']` → same `.content()/.forReader()/.toBytes()` accessors. For file-system uploads not on a record.
|
|
12
|
+
3. **Fetch by URL** — same `B.net.fetch(url)` pattern we use to inline styles.css; `B.csv(fetcher, fetcher.charset)`. Fallback only — reliability depends on URL + session auth. Prefer 1/2 for uploaded files.
|
|
13
|
+
|
|
14
|
+
**Permissions:** merge report runs in current user's context; routes 1/2 read via the platform object model subject to record/folder security.
|
|
15
|
+
|
|
16
|
+
**Gotchas:** strip UTF-8 BOM (`` corrupts first header key on Excel/QuickBooks exports); handle `\r\n`; sanitize currency (`$`, thousands commas, `(123)` negatives) before parseFloat; pass charset if Windows-1252; streaming `.forReader()` avoids buffering huge files and sidesteps the "code 0 but real content" issue seen with `B.io.fromInputStream`.
|
|
17
|
+
|
|
18
|
+
**Relevance:** unlocks the behavioral scorecard placeholder metrics that aren't in relational data — MRR, Monthly Profitability, Avg Contribution Margin (QuickBooks exports), NPS (survey results). Pattern: upload CSV to a Document field on a settings record; in app.ts `field.forReader(r => B.csv(r).toListOfObjects())`; the metric's Verify table can show the parsed rows. Docs: ~/.bluestep/docs/classes/CSV.md, DocumentLinkField.md, Document.md, Folder.md, B.Bluestep.IO.md.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Brandon's house design language for BlueStep dashboard merge reports — exact tokens (palette, type, cards, tabs). Use this for ALL new BlueStep dashboard UI; do NOT use Material/rounded-tonal styling"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Brandon's BlueStep dashboards (Census 1469219, Visit 1471499, DPN rop/1475679,
|
|
6
|
+
Authorization rop/1476379, and now Resources Hub 1471799) share ONE consistent
|
|
7
|
+
house style. Match it for any new dashboard/merge-report UI. **Not Material** — he
|
|
8
|
+
explicitly rejected the Material 3 look (nav rail, FAB, chunky tonal containers,
|
|
9
|
+
big radii, floating labels). Clean light enterprise, flat, data-forward.
|
|
10
|
+
|
|
11
|
+
**Palette (CSS vars):** primary `#1a6bb5`, primary-hover `#155d9e`, primary-active
|
|
12
|
+
`#104d85`; heading `#1a2d3f`; body `#2d3f50`; label `#4a5568`; muted `#7a8694`;
|
|
13
|
+
page bg `#f4f5f7`; surface `#fff`; alt-row `#f8f9fa`; row-hover `#e8f0fb`; border
|
|
14
|
+
`#dde1e7` (inputs `#c4cdd6`); table-header bg `#2d3f50` (white text). Status:
|
|
15
|
+
green `#137333`/bg`#e6f4ea`, yellow `#7a4f00`/bg`#fef7e0`, red `#c5221f`/bg`#fce8e6`,
|
|
16
|
+
gray `#5f6368`/bg`#f1f3f4` (also flat success `#28ae60`, warn `#f5b400`, err `#c03b2b`).
|
|
17
|
+
|
|
18
|
+
**Type:** system stack (`-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
|
19
|
+
'Helvetica Neue', Arial, sans-serif`) — NO web font. Base 14px. Page title 22px/700/
|
|
20
|
+
-0.3px. Section/card titles 15–16px/700. Labels 13px/600. Badges/pills 11px/700.
|
|
21
|
+
Table headers + stat labels often UPPERCASE, letter-spacing .04–.05em. Math: Menlo/Consolas.
|
|
22
|
+
|
|
23
|
+
**Layout:** max-width 1100–1280px, padding `24px 16px 48px`, sections ~20px apart.
|
|
24
|
+
|
|
25
|
+
**Cards/surfaces:** white, `1px solid #dde1e7`, radius 6px (8px tiles/modals), shadow
|
|
26
|
+
`0 1px 3px rgba(0,0,0,.06)` (modal `0 8px 32px rgba(0,0,0,.22)`). Depth from borders,
|
|
27
|
+
not heavy shadow. Signature motif: **4px colored left-border accent** on stat tiles /
|
|
28
|
+
cards / kanban columns.
|
|
29
|
+
|
|
30
|
+
**Nav:** horizontal **tab bar with 3px underline** on active (active color `#1a6bb5`,
|
|
31
|
+
hover bg `#f8f9fa`), border-bottom `1px #dde1e7`. Tables: dark `#2d3f50` sticky header,
|
|
32
|
+
zebra `#f8f9fa`, hover `#e8f0fb`. Pills: radius 12px (or 999px), 11px/700, color-coded
|
|
33
|
+
bg+fg. Buttons: primary `#1a6bb5` (hover `#155d9e`, focus ring `0 0 0 3px
|
|
34
|
+
rgba(26,107,181,.15-.25)`), outline = blue border that inverts on hover. Transitions
|
|
35
|
+
0.12–0.15s. No gradients (except subtle thumb backgrounds).
|
|
36
|
+
|
|
37
|
+
Relevant to bluestep resources; pairs with
|
|
38
|
+
[merge report static index](merge-report-static-index.md) and [separate files](../conventions/separate-files.md).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Use field.val(zonedDateTime) to set a DateTimeField; .dateTimeVal() has Graal-ambiguous overloads
|
|
3
|
+
---
|
|
4
|
+
**Use `field.val(zonedDateTime)` to write a DateTimeField. Do not use `.dateTimeVal(...)`.**
|
|
5
|
+
|
|
6
|
+
**Why:** `DateTimeField extends FormulaField<DateTimeField, java.time.ZonedDateTime>` — so the inherited `Field.val(T)` setter takes `ZonedDateTime` directly. The class also exposes a `dateTimeVal(...)` helper, but its Java side has two single-argument overloads: `dateTimeVal(LocalDateTime)` and `dateTimeVal(Instant)`. When you feed it a `ZonedDateTime` from `B.time.ZonedDateTime.now()`, Graal can't pick between the two and throws:
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
invokeMember (dateTimeVal) on ...DateTimeDataTypeFieldScript failed due to:
|
|
10
|
+
Multiple applicable overloads found for method name dateTimeVal
|
|
11
|
+
(candidates: [...dateTimeVal(LocalDateTime), ...dateTimeVal(Instant)],
|
|
12
|
+
arguments: [...ZonedDateTime])
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The class doc literally says `dateTimeVal(localDateTime, zoneId)` is *"the same as calling val(localDateTime.atZone(zoneId))"* — confirming `.val()` is the canonical setter.
|
|
16
|
+
|
|
17
|
+
**How to apply:**
|
|
18
|
+
- Setting "now": `entry.fields.someDateTime.val(B.time.ZonedDateTime.now());`
|
|
19
|
+
- Setting from parts: `entry.fields.someDateTime.val(localDateTime.atZone(zoneId));`
|
|
20
|
+
- Only reach for `.dateTimeVal(arg)` when you have a non-ambiguous argument:
|
|
21
|
+
- `.dateTimeVal(zdt.toInstant())` — explicit Instant works
|
|
22
|
+
- `.dateTimeVal(localDateTime, zoneId)` — two-arg overload is unambiguous
|
|
23
|
+
- `.dateTimeVal("2026-05-01T13:30:00-07:00")` — the third Java overload accepts a string the Relate parser handles
|
|
24
|
+
- For DateField (no time): use `field.val(localDate)` — same pattern, T = LocalDate.
|
|
25
|
+
- For TimeField: same pattern, T = LocalTime.
|
|
26
|
+
|
|
27
|
+
Confirmed on summitridge/1470299 audit-log endpoint, 2026-05-01.
|