@baravak/risloo-profile-cli 4.77.0 → 4.82.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/.claude/skills/create-profile/SKILL.md +36 -0
- package/.github/workflows/publish.yml +70 -0
- package/.github/workflows/push.yml +18 -9
- package/.mcp.json +8 -0
- package/AGENTS.md +14 -7
- package/package.json +2 -6
- package/src/samples/BSSI93.js +38 -8
- package/src/samples/NEO93.js +134 -15
- package/src/samples/NEO9A.js +6 -2
- package/src/samples/NEO9Q.js +146 -15
- package/src/samples/NEO9V.js +6 -2
- package/views/profiles/NEO_sheet.hbs +70 -0
- package/views/profiles/samples/BSSI93.hbs +10 -10
- package/views/profiles/samples/NEO93_1.hbs +1 -10
- package/views/profiles/samples/NEO93_2.hbs +9 -10
- package/views/profiles/samples/NEO93_3.hbs +1 -1
- package/views/profiles/samples/NEO93_4.hbs +15 -0
- package/views/profiles/samples/NEO9Q_1.hbs +1 -9
- package/views/profiles/samples/NEO9Q_2.hbs +8 -9
- package/views/profiles/samples/NEO9Q_3.hbs +1 -1
- package/views/profiles/samples/NEO9Q_4.hbs +14 -0
- package/.claude/settings.local.json +0 -7
- package/src/publish/bot.js +0 -91
- package/src/publish/new-version.hbs +0 -11
|
@@ -24,6 +24,42 @@ Ask for the relevant inputs:
|
|
|
24
24
|
|
|
25
25
|
Then **ask for the designer's Figma comments** — thresholds, coefficients, conditional behavior, spacing rules, and other implementation notes. The Figma MCP / SVG / HTML do **not** contain comments, and these specs usually live only there. Don't start coding until you have them.
|
|
26
26
|
|
|
27
|
+
### Figma MCP budget discipline — fetch the whole artifact, not pieces
|
|
28
|
+
|
|
29
|
+
Figma MCP read calls are **rate limited per plan and seat**, and the cheap tiers are brutal (a View/Collab seat, or any seat on a Starter plan, gets **6 calls per month**; a Dev/Full seat on Professional gets 200/day, 10/min). Running out mid-task strands the work. So treat every read call as expensive and design for **maximum data per request**.
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
|
|
33
|
+
1. **Download the complete artifact set up front — source, SVG, and PNG — then work locally.** Do not drip-feed one layer per call. Prefer `download_assets` on the **Chart** node: one call returns the whole-node export plus the vector SVGs of its subtree. Add `get_metadata` on the same node for the layer tree with exact `x/y/width/height`. Those two calls plus one `get_screenshot` give you geometry, vectors, and the visual reference for a whole page.
|
|
34
|
+
2. **Asset URLs cost nothing.** `get_design_context` on a vector-heavy layer returns an `<img src="…svg">` asset URL instead of code, and `get_screenshot` returns a PNG URL. Downloading those with `curl` does **not** consume quota. Always pull the file and read it locally rather than making another MCP call.
|
|
35
|
+
3. **Never call `get_design_context` on a repeated component instance more than once.** A dotted-column or dash-ruler instance can expand to hundreds of nodes, burning a call and flooding context. Sample **one** atom for its style, then derive the repetition from `get_metadata` sizes and the pitch arithmetic.
|
|
36
|
+
4. **Answer remaining questions from the downloaded PNG/SVG, not from Figma.** Exact colors, tick pitch, mirroring, and alignment are all measurable locally (e.g. sample pixels with Python/PIL, grep the SVG for `stroke=`/`fill=`). Go back to MCP only for text content and semantics that the outlined SVG genuinely cannot carry.
|
|
37
|
+
5. **Batch the calls you do need in one message** so they run in parallel and stay inside the per-minute cap; keep a batch at or below the seat's per-minute limit.
|
|
38
|
+
6. **Check the budget before a big pull.** `whoami` is exempt from rate limits and reports every plan and seat — use it when a call fails or before planning a large fetch. On a limit error, stop and tell the user which seat/plan is capping, rather than retrying.
|
|
39
|
+
7. **If the quota is exhausted, ask the user to export the files** (Chart SVG + PNG) instead of waiting — that path costs zero calls and the SVG is authoritative for color and geometry anyway.
|
|
40
|
+
|
|
41
|
+
#### Access tiers and which tools are metered
|
|
42
|
+
|
|
43
|
+
| Seat | Starter | Professional | Organization | Enterprise |
|
|
44
|
+
|---|---|---|---|---|
|
|
45
|
+
| View, Collab | 6 / month | 6 / month | 6 / month | 6 / month |
|
|
46
|
+
| Dev, Full | 6 / month | 200 / day, 10 / min | 200 / day, 15 / min | 600 / day, 20 / min |
|
|
47
|
+
|
|
48
|
+
- **Metered:** every tool that *reads* from Figma — `get_design_context`, `get_metadata`, `get_screenshot`, `get_variable_defs`, `download_assets`, and the rest.
|
|
49
|
+
- **Exempt:** `whoami`, `generate_figma_design`, `add_code_connect_map`, and other write-to-Figma tools. Reading MCP **resources** (`skill://…`, `file://figma/docs/…`) is also not metered.
|
|
50
|
+
- Quota follows the **plan that owns the file**, not the user's best seat. A user with a Full seat on their own Starter team and a View seat on the team that owns the design still gets 6/month for that design. `whoami` returns every plan with its `tier` and `seat`, plus the plan key — the plan key also appears in the rate-limit error URL, which identifies exactly which plan is capping.
|
|
51
|
+
- The fix is usually a **seat** upgrade (View → Dev/Full) on the owning team, not a plan upgrade. Report that distinction to the user instead of suggesting they buy a bigger plan.
|
|
52
|
+
|
|
53
|
+
### Recommended one-shot fetch sequence for a new page
|
|
54
|
+
|
|
55
|
+
For each Chart node, in a single batched message:
|
|
56
|
+
|
|
57
|
+
1. `get_metadata` — the layer tree with exact `x/y/width/height` for every node. This is the geometry ledger's backbone.
|
|
58
|
+
2. `download_assets` with `defaultFormat: "svg"` — returns a URL for the whole-node vector export plus the subtree's SVG assets.
|
|
59
|
+
3. `get_screenshot` at a `maxDimension` at least equal to the Chart's natural width — the visual acceptance reference.
|
|
60
|
+
|
|
61
|
+
Then `curl` all returned URLs (free) and do the rest locally: read the export SVG for exact colors, strokes, radii, gradients and paint order; read the metadata for layout boxes and text-node dimensions; compare renders against the PNG. Only text **content** needs a further call, and `get_metadata` already carries it in the layer `name` for text nodes.
|
|
62
|
+
|
|
27
63
|
Do not select one design artifact and ignore the others. Establish an authority table for the current task:
|
|
28
64
|
|
|
29
65
|
- **PNG** — final visual acceptance target, including visible orientation, wrapping, cropping, alignment, and composition.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
# Releases are driven by a tag:
|
|
4
|
+
#
|
|
5
|
+
# npm version <patch|minor|major>
|
|
6
|
+
# git push --follow-tags
|
|
7
|
+
#
|
|
8
|
+
# No npm token is involved. The job authenticates to the registry with a
|
|
9
|
+
# short-lived OIDC credential issued to this workflow (trusted publishing),
|
|
10
|
+
# which is what `id-token: write` below grants. The registry must be told to
|
|
11
|
+
# trust this repository + workflow filename in the package's settings.
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags:
|
|
15
|
+
- "v*"
|
|
16
|
+
workflow_dispatch:
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: read
|
|
20
|
+
id-token: write
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
publish:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v7
|
|
27
|
+
|
|
28
|
+
- uses: actions/setup-node@v7
|
|
29
|
+
with:
|
|
30
|
+
node-version: 24
|
|
31
|
+
registry-url: https://registry.npmjs.org
|
|
32
|
+
cache: npm
|
|
33
|
+
|
|
34
|
+
# Trusted publishing needs npm 11.5.1+. The npm bundled with Node usually
|
|
35
|
+
# already satisfies that, so only reach for the network when it does not
|
|
36
|
+
# — an unconditional global install is a needless failure point.
|
|
37
|
+
- name: Ensure npm supports trusted publishing
|
|
38
|
+
run: |
|
|
39
|
+
need=11.5.1
|
|
40
|
+
have="$(npm --version)"
|
|
41
|
+
if [ "$(printf '%s\n%s\n' "$need" "$have" | sort -V | head -n1)" = "$need" ]; then
|
|
42
|
+
echo "npm $have satisfies >= $need"
|
|
43
|
+
else
|
|
44
|
+
echo "npm $have is older than $need, upgrading"
|
|
45
|
+
for attempt in 1 2 3; do
|
|
46
|
+
npm install -g npm@latest && break
|
|
47
|
+
echo "attempt $attempt failed, retrying"; sleep 10
|
|
48
|
+
done
|
|
49
|
+
fi
|
|
50
|
+
npm --version
|
|
51
|
+
|
|
52
|
+
- run: npm ci --fetch-retries=5
|
|
53
|
+
|
|
54
|
+
# sharp carries a native binary. Fail here with a clear message rather
|
|
55
|
+
# than part-way through the release.
|
|
56
|
+
- name: Smoke-check the native image pipeline
|
|
57
|
+
run: node -e "require('sharp'); console.log('sharp loaded')"
|
|
58
|
+
|
|
59
|
+
# A tag that disagrees with package.json would publish the wrong version.
|
|
60
|
+
- name: Check the tag matches package.json
|
|
61
|
+
if: github.event_name == 'push'
|
|
62
|
+
run: |
|
|
63
|
+
tag="${GITHUB_REF_NAME#v}"
|
|
64
|
+
pkg="$(node -p "require('./package.json').version")"
|
|
65
|
+
echo "tag=$tag package.json=$pkg"
|
|
66
|
+
test "$tag" = "$pkg"
|
|
67
|
+
|
|
68
|
+
# Runs prepublishOnly (npm test) first. Provenance is attached
|
|
69
|
+
# automatically because this is a public repository.
|
|
70
|
+
- run: npm publish
|
|
@@ -1,14 +1,23 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
|
-
push
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
4
7
|
jobs:
|
|
5
|
-
|
|
8
|
+
test:
|
|
6
9
|
runs-on: ubuntu-latest
|
|
7
10
|
steps:
|
|
8
|
-
- uses: actions/checkout@
|
|
9
|
-
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-node@v7
|
|
10
14
|
with:
|
|
11
|
-
node-version:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
node-version: 24
|
|
16
|
+
cache: npm
|
|
17
|
+
|
|
18
|
+
# `npm ci` installs exactly what package-lock.json pins; the previous
|
|
19
|
+
# `yarn` step ignored that lockfile.
|
|
20
|
+
- run: npm ci
|
|
21
|
+
|
|
22
|
+
# Renders every sample in both variants.
|
|
23
|
+
- run: npm test
|
package/.mcp.json
ADDED
package/AGENTS.md
CHANGED
|
@@ -44,8 +44,7 @@ risloo-extractor-app/
|
|
|
44
44
|
│ └── publish/
|
|
45
45
|
│ ├── json/profiles/ # Template JSON per sample
|
|
46
46
|
│ ├── json/gift/ # Gift template data
|
|
47
|
-
│
|
|
48
|
-
│ └── bot.js # Post-publish automation
|
|
47
|
+
│ └── test.js # Auto-test all samples
|
|
49
48
|
├── views/
|
|
50
49
|
│ ├── profiles/samples/ # Handlebars SVG templates (.hbs)
|
|
51
50
|
│ └── gift.hbs
|
|
@@ -387,7 +386,7 @@ If several local coordinate patches accumulate, stop. Re-establish the coordinat
|
|
|
387
386
|
| Dates | Moment.js + moment-jalaali (Persian calendar) |
|
|
388
387
|
| QR codes | qrcode |
|
|
389
388
|
| File watching | Chokidar |
|
|
390
|
-
| Package manager |
|
|
389
|
+
| Package manager | npm (`package-lock.json` is the lockfile) |
|
|
391
390
|
| Design source | Figma (via MCP) |
|
|
392
391
|
|
|
393
392
|
---
|
|
@@ -405,9 +404,17 @@ If several local coordinate patches accumulate, stop. Re-establish the coordinat
|
|
|
405
404
|
|
|
406
405
|
## Publishing
|
|
407
406
|
|
|
407
|
+
Releases go through GitHub Actions via npm **trusted publishing** (OIDC) — there
|
|
408
|
+
is no npm token anywhere, local or in secrets. Tag the release and push it; the
|
|
409
|
+
`.github/workflows/publish.yml` job publishes it.
|
|
410
|
+
|
|
408
411
|
```bash
|
|
409
|
-
npm version <patch|minor|major>
|
|
410
|
-
|
|
411
|
-
# prepublishOnly: npm test
|
|
412
|
-
# postpublish: npm run bot
|
|
412
|
+
npm version <patch|minor|major> # bumps package.json and creates the vX.Y.Z tag
|
|
413
|
+
git push --follow-tags # pushing the tag triggers the release
|
|
414
|
+
# prepublishOnly: npm test — runs inside the job before the registry is touched
|
|
413
415
|
```
|
|
416
|
+
|
|
417
|
+
The job refuses to publish if the tag and `package.json` version disagree.
|
|
418
|
+
Publishing from a laptop with `npm publish` is not expected to work: npm
|
|
419
|
+
removed non-expiring tokens, and the package is configured to trust this
|
|
420
|
+
workflow instead.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@baravak/risloo-profile-cli",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.82.0",
|
|
4
4
|
"description": "**Risloo Profile CLI** is a library for creating profiles, reports and sheets for *psychological* samples.",
|
|
5
5
|
"main": "bin/risloo.js",
|
|
6
6
|
"publishConfig": {
|
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
"risloo": "bin/risloo.js"
|
|
11
11
|
},
|
|
12
12
|
"scripts": {
|
|
13
|
-
"bot": "node ./src/publish/bot.js",
|
|
14
13
|
"test": "node ./src/publish/test.js",
|
|
15
14
|
"prepublishOnly": "npm test",
|
|
16
|
-
"postpublish": "npm run bot",
|
|
17
15
|
"start": "node app"
|
|
18
16
|
},
|
|
19
17
|
"repository": {
|
|
@@ -26,8 +24,6 @@
|
|
|
26
24
|
"chalk": "^4.1.2",
|
|
27
25
|
"chokidar": "^3.5.3",
|
|
28
26
|
"commander": "^8.3.0",
|
|
29
|
-
"dotenv": "^16.0.0",
|
|
30
|
-
"form-data": "^4.0.0",
|
|
31
27
|
"handlebars": "^4.7.7",
|
|
32
28
|
"moment": "^2.29.1",
|
|
33
29
|
"moment-jalaali": "^0.9.2",
|
|
@@ -40,4 +36,4 @@
|
|
|
40
36
|
"url": "https://github.com/baravak/risloo-extractor-app/issues"
|
|
41
37
|
},
|
|
42
38
|
"homepage": "https://github.com/baravak/risloo-extractor-app#readme"
|
|
43
|
-
}
|
|
39
|
+
}
|
package/src/samples/BSSI93.js
CHANGED
|
@@ -58,14 +58,44 @@ class BSSI93 extends Profile {
|
|
|
58
58
|
const end = FS.toRadians(180); // 100% at left (9 o'clock)
|
|
59
59
|
total.zeta = total.p * (end - start) + start; // clockwise 270° sweep
|
|
60
60
|
|
|
61
|
-
// --- Factors (
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
61
|
+
// --- Factors (visual order, left → right) ---
|
|
62
|
+
// The scientific score keys stay unchanged. The design places the intent
|
|
63
|
+
// score in the middle slot and the preparation score in the right slot.
|
|
64
|
+
const factorSpecs = [
|
|
65
|
+
{
|
|
66
|
+
raw: s[2],
|
|
67
|
+
percentage: s[3],
|
|
68
|
+
cx: 120,
|
|
69
|
+
trackLeft: 106,
|
|
70
|
+
scoreX: 120.5,
|
|
71
|
+
lines: [{ text: "تمایل به مرگ", x: 120, y: 351.5 }],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
raw: s[6],
|
|
75
|
+
percentage: s[7],
|
|
76
|
+
cx: 220,
|
|
77
|
+
trackLeft: 206,
|
|
78
|
+
scoreX: 221.5,
|
|
79
|
+
lines: [
|
|
80
|
+
{ text: "آمادگی برای", x: 219.5, y: 351.5 },
|
|
81
|
+
{ text: "خودکشی", x: 219, y: 368 },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
raw: s[4],
|
|
86
|
+
percentage: s[5],
|
|
87
|
+
cx: 320,
|
|
88
|
+
trackLeft: 306,
|
|
89
|
+
scoreX: 322,
|
|
90
|
+
lines: [
|
|
91
|
+
{ text: "تمایل به", x: 322, y: 352.5 },
|
|
92
|
+
{ text: "خودکشی واقعی", x: 322, y: 368 },
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
const factors = factorSpecs.map(({ raw, percentage, ...geometry }) => ({
|
|
97
|
+
...packItem(raw, percentage),
|
|
98
|
+
...geometry,
|
|
69
99
|
}));
|
|
70
100
|
|
|
71
101
|
return [{ total, factors }];
|
package/src/samples/NEO93.js
CHANGED
|
@@ -52,12 +52,55 @@ for (const { key } of DOMAINS) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Page 4 (profile sheet). The Chart layer is 885 × 649; every number below is
|
|
56
|
+
// measured from it, so the shared NEO_sheet partial stays free of profile logic.
|
|
57
|
+
const SHEET_GROUP_X = [54, 175, 317, 459, 601, 743];
|
|
58
|
+
const SHEET_COL_STEP = 21;
|
|
59
|
+
const SHEET_COL_HALF = 6.5;
|
|
60
|
+
|
|
61
|
+
// 0 % sits on the bottom gridline, 100 % on the top one, 500 px apart.
|
|
62
|
+
const SHEET_ZERO_Y = 632;
|
|
63
|
+
const SHEET_PX_PER_PERCENT = 5;
|
|
64
|
+
|
|
65
|
+
const SHEET_DOMAIN_STROKE = "#4338CA";
|
|
66
|
+
const SHEET_FACET_STROKE = "#334155";
|
|
67
|
+
|
|
55
68
|
function clamp(value, min, max) {
|
|
56
69
|
const number = Number(value);
|
|
57
70
|
if (!Number.isFinite(number)) return min;
|
|
58
71
|
return Math.min(max, Math.max(min, number));
|
|
59
72
|
}
|
|
60
73
|
|
|
74
|
+
function sheetColumns() {
|
|
75
|
+
const columns = DOMAINS.map(({ fa, letter }, index) => ({
|
|
76
|
+
cx: SHEET_GROUP_X[0] + SHEET_COL_HALF + index * SHEET_COL_STEP,
|
|
77
|
+
code: letter,
|
|
78
|
+
fa,
|
|
79
|
+
domain: true,
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
DOMAINS.forEach(({ key, letter }, groupIndex) => {
|
|
83
|
+
FACET_NAMES[key].forEach((fa, index) => {
|
|
84
|
+
columns.push({
|
|
85
|
+
cx: SHEET_GROUP_X[groupIndex + 1] + SHEET_COL_HALF + index * SHEET_COL_STEP,
|
|
86
|
+
code: `${letter}${index + 1}`,
|
|
87
|
+
fa,
|
|
88
|
+
domain: false,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return columns;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// One dash per axis unit; every `accentEvery`-th one is the dark, readable tick.
|
|
97
|
+
function sheetDashes(top, pitch, count, accentEvery) {
|
|
98
|
+
return Array.from({ length: count }, (unused, index) => ({
|
|
99
|
+
y: top + index * pitch,
|
|
100
|
+
dark: index % accentEvery === 0,
|
|
101
|
+
}));
|
|
102
|
+
}
|
|
103
|
+
|
|
61
104
|
function layoutIndicators(indicators) {
|
|
62
105
|
let start = 0;
|
|
63
106
|
|
|
@@ -73,11 +116,12 @@ function layoutIndicators(indicators) {
|
|
|
73
116
|
}
|
|
74
117
|
|
|
75
118
|
class NEO93 extends Profile {
|
|
76
|
-
static pages =
|
|
119
|
+
static pages = 4;
|
|
77
120
|
|
|
78
121
|
static partials = {
|
|
79
122
|
NEO_main: "NEO_main.hbs",
|
|
80
123
|
NEO_long_facets: "NEO_long_facets.hbs",
|
|
124
|
+
NEO_sheet: "NEO_sheet.hbs",
|
|
81
125
|
};
|
|
82
126
|
|
|
83
127
|
labels = {
|
|
@@ -99,26 +143,35 @@ class NEO93 extends Profile {
|
|
|
99
143
|
},
|
|
100
144
|
profile: {
|
|
101
145
|
get dimensions() {
|
|
102
|
-
const [page1, page2, page3] = this.padding;
|
|
146
|
+
const [page1, page2, page3, page4] = this.padding;
|
|
103
147
|
return [
|
|
104
148
|
{
|
|
105
|
-
width:
|
|
106
|
-
height:
|
|
149
|
+
width: 885 + 2 * page1.x,
|
|
150
|
+
height: 649 + 2 * page1.y,
|
|
107
151
|
},
|
|
108
152
|
{
|
|
109
|
-
width:
|
|
110
|
-
height:
|
|
153
|
+
width: 800 + 2 * page2.x,
|
|
154
|
+
height: 674 + 2 * page2.y,
|
|
111
155
|
},
|
|
112
156
|
{
|
|
113
157
|
width: 811 + 2 * page3.x,
|
|
114
|
-
height:
|
|
158
|
+
height: 662 + 2 * page3.y,
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
width: 811 + 2 * page4.x,
|
|
162
|
+
height: 458 + 2 * page4.y,
|
|
115
163
|
},
|
|
116
164
|
];
|
|
117
165
|
},
|
|
166
|
+
// Padding is the Chart's inset inside the 943 × 754 design page *minus*
|
|
167
|
+
// the 20 px the layout already owns on every side. Every page therefore
|
|
168
|
+
// resolves to 903 × 714 — the exact with-sidebar drawing area — and
|
|
169
|
+
// renders at scale 1 instead of being shrunk to fit.
|
|
118
170
|
padding: [
|
|
119
|
-
{ x:
|
|
120
|
-
{ x:
|
|
121
|
-
{ x:
|
|
171
|
+
{ x: 9, y: 32.5 },
|
|
172
|
+
{ x: 51.5, y: 20 },
|
|
173
|
+
{ x: 46, y: 26 },
|
|
174
|
+
{ x: 46, y: 128 },
|
|
122
175
|
],
|
|
123
176
|
},
|
|
124
177
|
labels: Object.values(this.labels),
|
|
@@ -247,11 +300,77 @@ class NEO93 extends Profile {
|
|
|
247
300
|
};
|
|
248
301
|
});
|
|
249
302
|
|
|
303
|
+
const sheetCols = sheetColumns();
|
|
304
|
+
const sheetY = (percentage) => SHEET_ZERO_Y - SHEET_PX_PER_PERCENT * clamp(percentage, 0, 100);
|
|
305
|
+
const polyline = (cols, values) => values.map((value, index) => `${cols[index].cx},${sheetY(value)}`).join(" ");
|
|
306
|
+
|
|
307
|
+
const series = [
|
|
308
|
+
{ stroke: SHEET_DOMAIN_STROKE, points: polyline(sheetCols.slice(0, 5), items.map((item) => item.percentage)) },
|
|
309
|
+
...blocks.map((block, groupIndex) => ({
|
|
310
|
+
stroke: SHEET_FACET_STROKE,
|
|
311
|
+
points: polyline(
|
|
312
|
+
sheetCols.slice(5 + groupIndex * 6, 11 + groupIndex * 6),
|
|
313
|
+
block.facets.map((facet) => facet.percentage)
|
|
314
|
+
),
|
|
315
|
+
})),
|
|
316
|
+
];
|
|
317
|
+
|
|
318
|
+
// The profile sheet opens the report; the bar pages follow it.
|
|
250
319
|
return [
|
|
251
320
|
{
|
|
252
321
|
...sharedContext,
|
|
253
322
|
page: 1,
|
|
254
|
-
titleAppend:
|
|
323
|
+
titleAppend: " - کلاسیک",
|
|
324
|
+
columns: sheetCols,
|
|
325
|
+
series,
|
|
326
|
+
sheet: {
|
|
327
|
+
grid: {
|
|
328
|
+
x: 31,
|
|
329
|
+
w: 854,
|
|
330
|
+
ys: [
|
|
331
|
+
{ y: 131, faint: false },
|
|
332
|
+
{ y: 231, faint: true },
|
|
333
|
+
{ y: 331, faint: true },
|
|
334
|
+
{ y: 431, faint: true },
|
|
335
|
+
{ y: 531, faint: true },
|
|
336
|
+
{ y: 631, faint: false },
|
|
337
|
+
],
|
|
338
|
+
},
|
|
339
|
+
plot: { top: 132, bottom: 632 },
|
|
340
|
+
dot: { pitch: 5, accentPitch: 20, light: "#F1F5F9", accent: "#94A3B8" },
|
|
341
|
+
tick: { top: 121, h: 10 },
|
|
342
|
+
rail: {
|
|
343
|
+
dashLeftX: 36,
|
|
344
|
+
dashRightX: 873,
|
|
345
|
+
w: 6,
|
|
346
|
+
ruleLeftX: 45.5,
|
|
347
|
+
ruleRightX: 868.5,
|
|
348
|
+
ruleW: 1,
|
|
349
|
+
ruleTop: 131,
|
|
350
|
+
ruleH: 502,
|
|
351
|
+
dark: "#64748B",
|
|
352
|
+
light: "white",
|
|
353
|
+
},
|
|
354
|
+
dashes: sheetDashes(132, 5, 101, 4),
|
|
355
|
+
numberX: 24,
|
|
356
|
+
numbers: [
|
|
357
|
+
{ y: 132, text: "100 ٪" },
|
|
358
|
+
{ y: 232, text: "80 ٪" },
|
|
359
|
+
{ y: 332, text: "60 ٪" },
|
|
360
|
+
{ y: 432, text: "40 ٪" },
|
|
361
|
+
{ y: 532, text: "20 ٪" },
|
|
362
|
+
{ y: 632, text: "0" },
|
|
363
|
+
],
|
|
364
|
+
levelX: 0,
|
|
365
|
+
levels: [],
|
|
366
|
+
titleBottomY: 115,
|
|
367
|
+
codeY: 648,
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
...sharedContext,
|
|
372
|
+
page: 2,
|
|
373
|
+
titleAppend: titleAppend(2),
|
|
255
374
|
items,
|
|
256
375
|
redErrors: layoutIndicators(redErrors),
|
|
257
376
|
yellowErrors: layoutIndicators(yellowErrors),
|
|
@@ -260,15 +379,15 @@ class NEO93 extends Profile {
|
|
|
260
379
|
},
|
|
261
380
|
{
|
|
262
381
|
...sharedContext,
|
|
263
|
-
page:
|
|
264
|
-
titleAppend: titleAppend(
|
|
382
|
+
page: 3,
|
|
383
|
+
titleAppend: titleAppend(3),
|
|
265
384
|
blocks: blocks.slice(0, 3),
|
|
266
385
|
gridBottom: 658.5,
|
|
267
386
|
},
|
|
268
387
|
{
|
|
269
388
|
...sharedContext,
|
|
270
|
-
page:
|
|
271
|
-
titleAppend: titleAppend(
|
|
389
|
+
page: 4,
|
|
390
|
+
titleAppend: titleAppend(4),
|
|
272
391
|
blocks: blocks.slice(3, 5),
|
|
273
392
|
gridBottom: 454.5,
|
|
274
393
|
},
|
package/src/samples/NEO9A.js
CHANGED
|
@@ -66,9 +66,13 @@ class NEO9A extends Profile {
|
|
|
66
66
|
height: 674 + 2 * this.padding.y,
|
|
67
67
|
};
|
|
68
68
|
},
|
|
69
|
+
// Padding is the Chart's inset inside the 943 × 754 design page *minus*
|
|
70
|
+
// the 20 px the layout already owns on every side, so the page resolves
|
|
71
|
+
// to 903 × 714 — the exact with-sidebar drawing area — and renders at
|
|
72
|
+
// scale 1 instead of being shrunk to fit.
|
|
69
73
|
padding: {
|
|
70
|
-
x:
|
|
71
|
-
y:
|
|
74
|
+
x: 51.5,
|
|
75
|
+
y: 20,
|
|
72
76
|
},
|
|
73
77
|
},
|
|
74
78
|
labels: Object.values(this.labels),
|
package/src/samples/NEO9Q.js
CHANGED
|
@@ -52,12 +52,60 @@ for (const { key } of DOMAINS) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// Page 4 (profile sheet). The Chart layer is 878 × 704; every number below is
|
|
56
|
+
// measured from it, so the shared NEO_sheet partial stays free of profile logic.
|
|
57
|
+
const SHEET_GROUP_X = [47, 168, 310, 452, 594, 736];
|
|
58
|
+
const SHEET_COL_STEP = 21;
|
|
59
|
+
const SHEET_COL_HALF = 6.5;
|
|
60
|
+
|
|
61
|
+
// The T axis runs 20…80 at a flat 9 px per point: T 20 on the bottom gridline,
|
|
62
|
+
// T 80 on the top one. `_lookup` floors at 20 and the norm tables top out at 80,
|
|
63
|
+
// but the clamp also keeps a missing key (which `?? 0` turns into 0) on the sheet.
|
|
64
|
+
const SHEET_MIN_T = 20;
|
|
65
|
+
const SHEET_MAX_T = 80;
|
|
66
|
+
const SHEET_MIN_T_Y = 687;
|
|
67
|
+
const SHEET_PX_PER_T = 9;
|
|
68
|
+
|
|
69
|
+
const SHEET_DOMAIN_STROKE = "#4338CA";
|
|
70
|
+
const SHEET_FACET_STROKE = "#334155";
|
|
71
|
+
|
|
55
72
|
function clamp(value, min, max) {
|
|
56
73
|
const number = Number(value);
|
|
57
74
|
if (!Number.isFinite(number)) return min;
|
|
58
75
|
return Math.min(max, Math.max(min, number));
|
|
59
76
|
}
|
|
60
77
|
|
|
78
|
+
function sheetColumns() {
|
|
79
|
+
const columns = DOMAINS.map(({ fa, letter }, index) => ({
|
|
80
|
+
cx: SHEET_GROUP_X[0] + SHEET_COL_HALF + index * SHEET_COL_STEP,
|
|
81
|
+
code: letter,
|
|
82
|
+
fa,
|
|
83
|
+
domain: true,
|
|
84
|
+
}));
|
|
85
|
+
|
|
86
|
+
DOMAINS.forEach(({ key, letter }, groupIndex) => {
|
|
87
|
+
FACET_NAMES[key].forEach((fa, index) => {
|
|
88
|
+
columns.push({
|
|
89
|
+
cx: SHEET_GROUP_X[groupIndex + 1] + SHEET_COL_HALF + index * SHEET_COL_STEP,
|
|
90
|
+
code: `${letter}${index + 1}`,
|
|
91
|
+
fa,
|
|
92
|
+
domain: false,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
return columns;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// One dash per T point; every `accentEvery`-th one is dark — on this form that
|
|
101
|
+
// marks the even T values, which are the only ones the norm tables can produce.
|
|
102
|
+
function sheetDashes(top, pitch, count, accentEvery) {
|
|
103
|
+
return Array.from({ length: count }, (unused, index) => ({
|
|
104
|
+
y: top + index * pitch,
|
|
105
|
+
dark: index % accentEvery === 0,
|
|
106
|
+
}));
|
|
107
|
+
}
|
|
108
|
+
|
|
61
109
|
function layoutIndicators(indicators) {
|
|
62
110
|
let start = 0;
|
|
63
111
|
|
|
@@ -73,11 +121,12 @@ function layoutIndicators(indicators) {
|
|
|
73
121
|
}
|
|
74
122
|
|
|
75
123
|
class NEO9Q extends Profile {
|
|
76
|
-
static pages =
|
|
124
|
+
static pages = 4;
|
|
77
125
|
|
|
78
126
|
static partials = {
|
|
79
127
|
NEO_main: "NEO_main.hbs",
|
|
80
128
|
NEO_long_facets: "NEO_long_facets.hbs",
|
|
129
|
+
NEO_sheet: "NEO_sheet.hbs",
|
|
81
130
|
};
|
|
82
131
|
|
|
83
132
|
labels = {
|
|
@@ -99,26 +148,35 @@ class NEO9Q extends Profile {
|
|
|
99
148
|
},
|
|
100
149
|
profile: {
|
|
101
150
|
get dimensions() {
|
|
102
|
-
const [page1, page2, page3] = this.padding;
|
|
151
|
+
const [page1, page2, page3, page4] = this.padding;
|
|
103
152
|
return [
|
|
104
153
|
{
|
|
105
|
-
width:
|
|
106
|
-
height:
|
|
154
|
+
width: 878 + 2 * page1.x,
|
|
155
|
+
height: 704 + 2 * page1.y,
|
|
107
156
|
},
|
|
108
157
|
{
|
|
109
|
-
width:
|
|
110
|
-
height:
|
|
158
|
+
width: 800 + 2 * page2.x,
|
|
159
|
+
height: 674 + 2 * page2.y,
|
|
111
160
|
},
|
|
112
161
|
{
|
|
113
162
|
width: 811 + 2 * page3.x,
|
|
114
|
-
height:
|
|
163
|
+
height: 668 + 2 * page3.y,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
width: 811 + 2 * page4.x,
|
|
167
|
+
height: 464 + 2 * page4.y,
|
|
115
168
|
},
|
|
116
169
|
];
|
|
117
170
|
},
|
|
171
|
+
// Padding is the Chart's inset inside the 943 × 754 design page *minus*
|
|
172
|
+
// the 20 px the layout already owns on every side. Every page therefore
|
|
173
|
+
// resolves to 903 × 714 — the exact with-sidebar drawing area — and
|
|
174
|
+
// renders at scale 1 instead of being shrunk to fit.
|
|
118
175
|
padding: [
|
|
119
|
-
{ x:
|
|
120
|
-
{ x:
|
|
121
|
-
{ x:
|
|
176
|
+
{ x: 12.5, y: 5 },
|
|
177
|
+
{ x: 51.5, y: 20 },
|
|
178
|
+
{ x: 46, y: 23 },
|
|
179
|
+
{ x: 46, y: 125 },
|
|
122
180
|
],
|
|
123
181
|
},
|
|
124
182
|
labels: Object.values(this.labels),
|
|
@@ -249,11 +307,84 @@ class NEO9Q extends Profile {
|
|
|
249
307
|
};
|
|
250
308
|
});
|
|
251
309
|
|
|
310
|
+
const sheetCols = sheetColumns();
|
|
311
|
+
const sheetY = (t) => SHEET_MIN_T_Y - SHEET_PX_PER_T * (clamp(t, SHEET_MIN_T, SHEET_MAX_T) - SHEET_MIN_T);
|
|
312
|
+
const polyline = (cols, values) => values.map((value, index) => `${cols[index].cx},${sheetY(value)}`).join(" ");
|
|
313
|
+
|
|
314
|
+
const series = [
|
|
315
|
+
{ stroke: SHEET_DOMAIN_STROKE, points: polyline(sheetCols.slice(0, 5), items.map((item) => item.tScore)) },
|
|
316
|
+
...blocks.map((block, groupIndex) => ({
|
|
317
|
+
stroke: SHEET_FACET_STROKE,
|
|
318
|
+
points: polyline(
|
|
319
|
+
sheetCols.slice(5 + groupIndex * 6, 11 + groupIndex * 6),
|
|
320
|
+
block.facets.map((facet) => facet.tScore)
|
|
321
|
+
),
|
|
322
|
+
})),
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
// The profile sheet opens the report; the bar pages follow it.
|
|
252
326
|
return [
|
|
253
327
|
{
|
|
254
328
|
...sharedContext,
|
|
255
329
|
page: 1,
|
|
256
|
-
titleAppend:
|
|
330
|
+
titleAppend: " - کلاسیک",
|
|
331
|
+
columns: sheetCols,
|
|
332
|
+
series,
|
|
333
|
+
sheet: {
|
|
334
|
+
grid: {
|
|
335
|
+
x: 24,
|
|
336
|
+
w: 854,
|
|
337
|
+
ys: [
|
|
338
|
+
{ y: 146, faint: false },
|
|
339
|
+
{ y: 281, faint: true },
|
|
340
|
+
{ y: 371, faint: true },
|
|
341
|
+
{ y: 461, faint: true },
|
|
342
|
+
{ y: 551, faint: true },
|
|
343
|
+
{ y: 686, faint: false },
|
|
344
|
+
],
|
|
345
|
+
},
|
|
346
|
+
plot: { top: 147, bottom: 687 },
|
|
347
|
+
dot: { pitch: 9, accentPitch: 18, light: "#F1F5F9", accent: "#94A3B8" },
|
|
348
|
+
tick: { top: 136, h: 10 },
|
|
349
|
+
rail: {
|
|
350
|
+
dashLeftX: 29,
|
|
351
|
+
dashRightX: 866,
|
|
352
|
+
w: 6,
|
|
353
|
+
ruleLeftX: 38,
|
|
354
|
+
ruleRightX: 861,
|
|
355
|
+
ruleW: 2,
|
|
356
|
+
ruleTop: 146,
|
|
357
|
+
ruleH: 542,
|
|
358
|
+
dark: "#64748B",
|
|
359
|
+
light: "#CBD5E1",
|
|
360
|
+
},
|
|
361
|
+
dashes: sheetDashes(147, 9, 61, 2),
|
|
362
|
+
numberX: 17,
|
|
363
|
+
numbers: [
|
|
364
|
+
{ y: 147, text: "80" },
|
|
365
|
+
{ y: 282, text: "65" },
|
|
366
|
+
{ y: 372, text: "55" },
|
|
367
|
+
{ y: 462, text: "45" },
|
|
368
|
+
{ y: 552, text: "35" },
|
|
369
|
+
{ y: 687, text: "20" },
|
|
370
|
+
],
|
|
371
|
+
levelX: 9.9,
|
|
372
|
+
// Each band label sits midway between the two gridlines that bound it.
|
|
373
|
+
levels: [
|
|
374
|
+
{ y: 214.5, text: LEVELS[5] },
|
|
375
|
+
{ y: 327, text: LEVELS[4] },
|
|
376
|
+
{ y: 417, text: LEVELS[3] },
|
|
377
|
+
{ y: 507, text: LEVELS[2] },
|
|
378
|
+
{ y: 619.5, text: LEVELS[1] },
|
|
379
|
+
],
|
|
380
|
+
titleBottomY: 130,
|
|
381
|
+
codeY: 703,
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
...sharedContext,
|
|
386
|
+
page: 2,
|
|
387
|
+
titleAppend: titleAppend(2),
|
|
257
388
|
items,
|
|
258
389
|
redErrors: layoutIndicators(redErrors),
|
|
259
390
|
yellowErrors: layoutIndicators(yellowErrors),
|
|
@@ -262,15 +393,15 @@ class NEO9Q extends Profile {
|
|
|
262
393
|
},
|
|
263
394
|
{
|
|
264
395
|
...sharedContext,
|
|
265
|
-
page:
|
|
266
|
-
titleAppend: titleAppend(
|
|
396
|
+
page: 3,
|
|
397
|
+
titleAppend: titleAppend(3),
|
|
267
398
|
blocks: blocks.slice(0, 3),
|
|
268
399
|
gridBottom: 664.5,
|
|
269
400
|
},
|
|
270
401
|
{
|
|
271
402
|
...sharedContext,
|
|
272
|
-
page:
|
|
273
|
-
titleAppend: titleAppend(
|
|
403
|
+
page: 4,
|
|
404
|
+
titleAppend: titleAppend(4),
|
|
274
405
|
blocks: blocks.slice(3, 5),
|
|
275
406
|
gridBottom: 460.5,
|
|
276
407
|
},
|
package/src/samples/NEO9V.js
CHANGED
|
@@ -132,9 +132,13 @@ class NEO9V extends Profile {
|
|
|
132
132
|
},
|
|
133
133
|
];
|
|
134
134
|
},
|
|
135
|
+
// Padding is the Chart's inset inside the 943 × 754 design page *minus*
|
|
136
|
+
// the 20 px the layout already owns on every side. Both pages therefore
|
|
137
|
+
// resolve to 903 × 714 — the exact with-sidebar drawing area — and
|
|
138
|
+
// render at scale 1 instead of being shrunk to fit.
|
|
135
139
|
padding: [
|
|
136
|
-
{ x:
|
|
137
|
-
{ x:
|
|
140
|
+
{ x: 51, y: 20 },
|
|
141
|
+
{ x: 71.5, y: 26 },
|
|
138
142
|
],
|
|
139
143
|
},
|
|
140
144
|
labels: Object.values(this.labels),
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{{!--
|
|
2
|
+
NEO profile sheet (نیمرخ): 35 dotted columns — the 5 domains, then 6 groups of
|
|
3
|
+
6 facets — over a horizontal gridline scale, with rotated titles above, latin
|
|
4
|
+
codes below, and one polyline per group.
|
|
5
|
+
|
|
6
|
+
Every coordinate arrives from the controller; nothing is derived here, so the
|
|
7
|
+
percentage sheet and the T-score sheet share this file without a variant flag.
|
|
8
|
+
|
|
9
|
+
padding {x, y} page padding owned by the layout
|
|
10
|
+
sheet.grid {x, w, ys: [{y, faint}]} gridline rects, y = rect top edge
|
|
11
|
+
sheet.plot {top, bottom} dot-column endpoints (gridline centres)
|
|
12
|
+
sheet.dot {pitch, accentPitch, light, accent}
|
|
13
|
+
sheet.tick {top, h} column ticks above the top gridline
|
|
14
|
+
sheet.rail {dashLeftX, dashRightX, w, ruleLeftX, ruleRightX, ruleW,
|
|
15
|
+
ruleTop, ruleH, dark, light, dashes: [{y, dark}]}
|
|
16
|
+
sheet.numberX right edge the axis numbers align to
|
|
17
|
+
sheet.numbers [{y, text}] axis numbers, y = gridline centre
|
|
18
|
+
sheet.levelX centre guide of the rotated level words
|
|
19
|
+
sheet.levels [{y, text}] rotated level words, may be empty
|
|
20
|
+
sheet.titleBottomY guide the rotated titles grow upward from
|
|
21
|
+
sheet.codeY baseline of the latin codes
|
|
22
|
+
columns [{cx, code, fa, domain}] 35 entries, design draw order
|
|
23
|
+
series [{points, stroke}] one polyline per group
|
|
24
|
+
|
|
25
|
+
Paint order mirrors the design export: codes, titles, dots, rails, numbers,
|
|
26
|
+
ticks, gridlines, result lines.
|
|
27
|
+
--}}
|
|
28
|
+
<g transform="translate({{padding.x}}, {{padding.y}})">
|
|
29
|
+
{{#each columns as |col|}}
|
|
30
|
+
<text class="eng-font" x="{{col.cx}}" y="{{../sheet.codeY}}" direction="ltr" font-size="10" font-weight="400" fill="#94A3B8" text-anchor="middle">{{col.code}}</text>
|
|
31
|
+
{{/each}}
|
|
32
|
+
|
|
33
|
+
{{#each columns as |col|}}
|
|
34
|
+
<text x="{{col.cx}}" y="{{../sheet.titleBottomY}}" transform="rotate(-90 {{col.cx}} {{../sheet.titleBottomY}})" direction="ltr" font-size="12" font-weight="{{ternary col.domain 600 400}}" fill="{{ternary col.domain '#4338CA' '#334155'}}" text-anchor="start" dy=".35em"><tspan class="eng-font">{{col.code}}</tspan> - {{col.fa}}</text>
|
|
35
|
+
{{/each}}
|
|
36
|
+
|
|
37
|
+
{{#each columns as |col|}}
|
|
38
|
+
<line x1="{{col.cx}}" y1="{{../sheet.plot.top}}" x2="{{col.cx}}" y2="{{../sheet.plot.bottom}}" stroke="{{../sheet.dot.light}}" stroke-width="2" stroke-linecap="round" stroke-dasharray="0 {{../sheet.dot.pitch}}"/>
|
|
39
|
+
<line x1="{{col.cx}}" y1="{{../sheet.plot.top}}" x2="{{col.cx}}" y2="{{../sheet.plot.bottom}}" stroke="{{../sheet.dot.accent}}" stroke-width="2" stroke-linecap="round" stroke-dasharray="0 {{../sheet.dot.accentPitch}}"/>
|
|
40
|
+
{{/each}}
|
|
41
|
+
|
|
42
|
+
{{#each sheet.dashes as |dash|}}
|
|
43
|
+
<line x1="{{math ../sheet.rail.dashLeftX '+' 1}}" y1="{{dash.y}}" x2="{{math ../sheet.rail.dashLeftX '+' (math ../sheet.rail.w '-' 1)}}" y2="{{dash.y}}" stroke="{{ternary dash.dark ../sheet.rail.dark ../sheet.rail.light}}" stroke-width="2" stroke-linecap="round"/>
|
|
44
|
+
<line x1="{{math ../sheet.rail.dashRightX '+' 1}}" y1="{{dash.y}}" x2="{{math ../sheet.rail.dashRightX '+' (math ../sheet.rail.w '-' 1)}}" y2="{{dash.y}}" stroke="{{ternary dash.dark ../sheet.rail.dark ../sheet.rail.light}}" stroke-width="2" stroke-linecap="round"/>
|
|
45
|
+
{{/each}}
|
|
46
|
+
<rect x="{{sheet.rail.ruleLeftX}}" y="{{sheet.rail.ruleTop}}" width="{{sheet.rail.ruleW}}" height="{{sheet.rail.ruleH}}" fill="{{sheet.rail.dark}}"/>
|
|
47
|
+
<rect x="{{sheet.rail.ruleRightX}}" y="{{sheet.rail.ruleTop}}" width="{{sheet.rail.ruleW}}" height="{{sheet.rail.ruleH}}" fill="{{sheet.rail.dark}}"/>
|
|
48
|
+
|
|
49
|
+
{{#each sheet.numbers as |number|}}
|
|
50
|
+
<text x="{{../sheet.numberX}}" y="{{number.y}}" dy=".3em" font-size="12" font-weight="400" fill="#64748B" text-anchor="start">{{number.text}}</text>
|
|
51
|
+
{{/each}}
|
|
52
|
+
|
|
53
|
+
{{#each sheet.levels as |level|}}
|
|
54
|
+
<text x="{{../sheet.levelX}}" y="{{level.y}}" transform="rotate(-90 {{../sheet.levelX}} {{level.y}})" font-size="12" font-weight="400" fill="#94A3B8" text-anchor="middle" dy=".35em">{{level.text}}</text>
|
|
55
|
+
{{/each}}
|
|
56
|
+
|
|
57
|
+
{{#each columns as |col|}}
|
|
58
|
+
<g transform="translate({{math col.cx '-' 1}}, {{../sheet.tick.top}})">
|
|
59
|
+
{{bar 2 ../sheet.tick.h (object tl=1 tr=1 bl=0 br=0) (toRad 0) fill=(ternary col.domain '#4338CA' '#64748B')}}
|
|
60
|
+
</g>
|
|
61
|
+
{{/each}}
|
|
62
|
+
|
|
63
|
+
{{#each sheet.grid.ys as |line|}}
|
|
64
|
+
<rect x="{{../sheet.grid.x}}" y="{{line.y}}" width="{{../sheet.grid.w}}" height="2" rx="1" fill="#94A3B8"{{#if line.faint}} fill-opacity="0.25"{{/if}}/>
|
|
65
|
+
{{/each}}
|
|
66
|
+
|
|
67
|
+
{{#each series as |line|}}
|
|
68
|
+
<polyline points="{{line.points}}" fill="none" stroke="{{line.stroke}}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
69
|
+
{{/each}}
|
|
70
|
+
</g>
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
<line x1="69.5" y1="102.5" x2="408.5" y2="102.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
|
|
48
48
|
<line x1="69.5" y1="177.5" x2="408.5" y2="177.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
|
|
49
49
|
<line x1="69.5" y1="252.5" x2="408.5" y2="252.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
|
|
50
|
-
<text x="53" y="
|
|
51
|
-
<text x="53" y="
|
|
50
|
+
<text x="53" y="25.5" text-anchor="end" dy=".85em" font-size="12" fill="#94A3B8" direction="ltr">٪ 100</text>
|
|
51
|
+
<text x="53" y="175.5" text-anchor="end" dy=".35em" font-size="12" fill="#94A3B8" direction="ltr">٪ 50</text>
|
|
52
52
|
|
|
53
53
|
{{! Value bars + inside/outside percentage label }}
|
|
54
54
|
{{#each factors as |factor|}}
|
|
@@ -66,8 +66,8 @@
|
|
|
66
66
|
|
|
67
67
|
{{! Domain names under each bar }}
|
|
68
68
|
{{#each factors as |factor|}}
|
|
69
|
-
{{#each factor.lines as |line
|
|
70
|
-
<text x="{{
|
|
69
|
+
{{#each factor.lines as |line|}}
|
|
70
|
+
<text x="{{line.x}}" y="{{line.y}}" text-anchor="middle" font-size="14" fill="#334155">{{line.text}}</text>
|
|
71
71
|
{{/each}}
|
|
72
72
|
{{/each}}
|
|
73
73
|
|
|
@@ -77,10 +77,10 @@
|
|
|
77
77
|
</g>
|
|
78
78
|
|
|
79
79
|
{{! ===== "raw score" header row: legend + per-bar raw/max fractions ===== }}
|
|
80
|
-
<text x="38" y="
|
|
80
|
+
<text x="38" y="7.5" text-anchor="start" dy=".3em" font-size="12" fill="#94A3B8">نمره خام</text>
|
|
81
81
|
<path d="M45.5 8L52.5 8M49.5 11L52.5 8L49.5 5" stroke="#94A3B8" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
|
82
82
|
{{#each factors as |factor|}}
|
|
83
|
-
<text x="{{factor.
|
|
83
|
+
<text x="{{factor.scoreX}}" y="9.5" text-anchor="middle" dy=".3em" direction="ltr"><tspan font-weight="700" font-size="16" fill="#BE185D">{{factor.mark}}</tspan><tspan font-weight="300" font-size="12" fill="#64748B"> / </tspan><tspan font-weight="400" font-size="13" fill="#64748B">{{factor.max}}</tspan></text>
|
|
84
84
|
{{/each}}
|
|
85
85
|
|
|
86
86
|
{{! ===================== Total score ring gauge ===================== }}
|
|
@@ -88,13 +88,13 @@
|
|
|
88
88
|
{{gauge 70 49 (object tl=6 tr=6 bl=6 br=6) (object start=(toRad -90) end=(toRad 180)) false clip-path="url(#gauge-roll)" fill="#F1F5F9"}}
|
|
89
89
|
{{#if total.mark}}{{gauge 70 49 (object tl=6 tr=6 bl=6 br=6) (object start=(toRad -90) end=total.zeta) false clip-path="url(#gauge-roll)" fill="url(#gaugeGrad)"}}{{/if}}
|
|
90
90
|
|
|
91
|
-
<text x="0" y="-
|
|
92
|
-
<text x="0" y="
|
|
91
|
+
<text x="0" y="-4" text-anchor="middle" font-size="24" font-weight="600" fill="#334155" direction="ltr">٪ {{total.pct}}</text>
|
|
92
|
+
<text x="0" y="22" text-anchor="middle" font-size="15" font-weight="500" fill="#64748B">{{total.mark}} از {{total.max}}</text>
|
|
93
93
|
|
|
94
|
-
<text x="-60" y="-
|
|
94
|
+
<text x="-60" y="-8" text-anchor="middle" font-size="14" fill="#64748B" direction="ltr">٪ 100</text>
|
|
95
95
|
<text x="-11" y="-56" text-anchor="middle" font-size="14" fill="#64748B">0</text>
|
|
96
96
|
|
|
97
|
-
<text x="0" y="
|
|
97
|
+
<text x="0" y="102" text-anchor="middle" font-size="16" font-weight="500" fill="#475569">نمره کل</text>
|
|
98
98
|
</g>
|
|
99
99
|
|
|
100
100
|
</g>
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
{{#> layout}}
|
|
2
|
-
{{>
|
|
2
|
+
{{> NEO_main
|
|
3
3
|
padding=spec.profile.padding.[1]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rawX=
|
|
7
|
-
rawCenter=
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
gridTop=23.5
|
|
12
|
-
gridBottom=658.5
|
|
4
|
+
frameX=32
|
|
5
|
+
barX=111
|
|
6
|
+
rawX=627.5
|
|
7
|
+
rawCenter=645
|
|
8
|
+
rawMax=192
|
|
9
|
+
levelX=707
|
|
10
|
+
showLevels=true
|
|
13
11
|
tAxis=false
|
|
12
|
+
alertX=0
|
|
14
13
|
}}
|
|
15
14
|
{{/layout}}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
{{#> layout}}
|
|
2
|
-
{{>
|
|
2
|
+
{{> NEO_main
|
|
3
3
|
padding=spec.profile.padding.[1]
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
rawX=
|
|
7
|
-
rawCenter=
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
gridTop=29.5
|
|
11
|
-
gridBottom=664.5
|
|
4
|
+
frameX=62
|
|
5
|
+
barX=123
|
|
6
|
+
rawX=639.5
|
|
7
|
+
rawCenter=657
|
|
8
|
+
rawMax=192
|
|
9
|
+
showLevels=false
|
|
12
10
|
tAxis=true
|
|
11
|
+
alertX=0
|
|
13
12
|
}}
|
|
14
13
|
{{/layout}}
|
package/src/publish/bot.js
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
const fs = require("fs/promises");
|
|
2
|
-
const FormData = require("form-data");
|
|
3
|
-
const path = require("path");
|
|
4
|
-
const sharp = require("sharp");
|
|
5
|
-
const Handlebars = require("handlebars");
|
|
6
|
-
const readline = require("readline");
|
|
7
|
-
const https = require("https");
|
|
8
|
-
require("dotenv").config();
|
|
9
|
-
|
|
10
|
-
// New version image hbs directory
|
|
11
|
-
const templateDir = path.join(__dirname, "new-version.hbs");
|
|
12
|
-
|
|
13
|
-
// Version to be Released
|
|
14
|
-
// Remember you should run this script using 'npm ...' command to have npm environment variables
|
|
15
|
-
const version = process.env.npm_package_version;
|
|
16
|
-
|
|
17
|
-
// Telegram Bot Info (Bot Token Should Be Manually Provided Using dotenv)
|
|
18
|
-
const token = process.env.TELEGRAM_BOT_TOKEN;
|
|
19
|
-
const chatId = "-651191778";
|
|
20
|
-
|
|
21
|
-
const rl = readline.createInterface({
|
|
22
|
-
input: process.stdin,
|
|
23
|
-
output: process.stdout,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const releaseNotes = [];
|
|
27
|
-
|
|
28
|
-
rl.setPrompt("Release Notes:\n");
|
|
29
|
-
|
|
30
|
-
rl.prompt();
|
|
31
|
-
|
|
32
|
-
rl.on("line", function (ln) {
|
|
33
|
-
releaseNotes.push(ln);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// Press Ctrl+C to Just Exit
|
|
37
|
-
rl.on("SIGINT", () => process.exit(0));
|
|
38
|
-
|
|
39
|
-
// Press Ctrl+D to Emit 'close' Event and Send Release Notes
|
|
40
|
-
rl.on("close", sendReleaseNotes);
|
|
41
|
-
|
|
42
|
-
// Returns Buffer of New Version Image
|
|
43
|
-
async function createReleaseImage() {
|
|
44
|
-
return fs
|
|
45
|
-
.readFile(templateDir)
|
|
46
|
-
.then((templateBuffer) => {
|
|
47
|
-
const template = Handlebars.compile(templateBuffer.toString(), "utf-8");
|
|
48
|
-
let ctx = {
|
|
49
|
-
version,
|
|
50
|
-
};
|
|
51
|
-
const xml = template(ctx);
|
|
52
|
-
|
|
53
|
-
const buf = Buffer.from(xml, "utf8");
|
|
54
|
-
return sharp(buf).resize({ width: 350 }).jpeg({ quality: 100 }).toBuffer();
|
|
55
|
-
})
|
|
56
|
-
.catch((err) => {
|
|
57
|
-
throw err;
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function sendReleaseNotes() {
|
|
62
|
-
// Text to be Sent
|
|
63
|
-
const caption = `Version *${version}* Released!\n\n_Release Notes:_\n${releaseNotes.join("\n")}`;
|
|
64
|
-
const photoBuf = await createReleaseImage();
|
|
65
|
-
|
|
66
|
-
let form = new FormData();
|
|
67
|
-
|
|
68
|
-
form.append("chat_id", chatId);
|
|
69
|
-
form.append("photo", photoBuf, { filename: "image.jpeg" });
|
|
70
|
-
form.append("caption", caption);
|
|
71
|
-
form.append("parse_mode", "Markdown");
|
|
72
|
-
|
|
73
|
-
// HTTP Request Options (URL & Headers & Method)
|
|
74
|
-
const options = {
|
|
75
|
-
hostname: "api.telegram.org",
|
|
76
|
-
path: `/bot${token}/sendPhoto`,
|
|
77
|
-
protocol: "https:",
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
form.submit(options, (err, res) => {
|
|
81
|
-
console.log(`statusCode: ${res.statusCode}`);
|
|
82
|
-
|
|
83
|
-
res.on("data", (d) => {
|
|
84
|
-
process.stdout.write(d);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
if (err) console.log(err);
|
|
88
|
-
|
|
89
|
-
res.emit("end");
|
|
90
|
-
});
|
|
91
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
-
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 400" font-family="Dana">
|
|
3
|
-
<rect x="0" y="0" width="500" height="400" fill="#EA6B13"/>
|
|
4
|
-
<g transform="translate(250, 200)">
|
|
5
|
-
<text x="0" y="-75" fill="white" fill-opacity="0.25" font-size="36" font-weight="800" text-anchor="middle">RISLOO EXTRACTOR CLI</text>
|
|
6
|
-
<g transform="translate(0, 65)">
|
|
7
|
-
<text x="0" y="0" fill="black" fill-opacity="0.25" font-size="88" font-weight="900" text-anchor="middle">v {{version}}</text>
|
|
8
|
-
<text x="0" y="0" fill="white" font-size="96" font-weight="900" text-anchor="middle">v {{version}}</text>
|
|
9
|
-
</g>
|
|
10
|
-
</g>
|
|
11
|
-
</svg>
|