@baravak/risloo-profile-cli 4.87.0 → 4.88.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baravak/risloo-profile-cli",
3
- "version": "4.87.0",
3
+ "version": "4.88.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": {
@@ -1,28 +1,29 @@
1
1
  const { Profile } = require("../Profile");
2
2
 
3
3
  // ---------------------------------------------------------------------------
4
- // PSQI93 — شاخص کیفیت خواب پیتزبورگ. No Figma source: the chart was designed
5
- // directly from the approved result specification of the science record
6
- // (catalog/assessments/psqi, «مشخصات نتیجه فردی»), whose rules this layout
7
- // enforces rather than decorates:
4
+ // PSQI93 — شاخص کیفیت خواب پیتزبورگ. Layout from the Figma "Chart" layer of
5
+ // the JSS93 design (Assessments v3.0.0, node 8071:183989), adapted to seven
6
+ // rows on a 0–3 axis with no raw-score column, and a vertical 0–21 total
7
+ // column. Rules kept from the approved result specification of the science
8
+ // record (catalog/assessments/psqi, «مشخصات نتیجه فردی»):
8
9
  // - seven components on one shared 0–3 axis, raw only — no per-component
9
10
  // class, colour or label (no evidence for any);
10
- // - the total 0–21, drawn neutral: the screening result lives ONLY in the
11
- // approved interpretation text — no badge, no colour, no cut line;
11
+ // - the total 0–21 with the screening cut line at 6: 0–5 neutral, 6–21 red;
12
12
  // - all-or-nothing: when scoring/PSQI93.py returns status "invalid" nothing
13
13
  // but the no-result message is drawn — never a partial or zero score.
14
- // Chart 863 × 674, centred on the 943 × 754 design page.
14
+ // Chart 789 × 334: the design's 789 × 414 less two of its 40 px rows, centred
15
+ // on the 943 × 754 design page.
15
16
  // ---------------------------------------------------------------------------
16
- const CHART = { width: 863, height: 674 };
17
+ const CHART = { width: 789, height: 334 };
17
18
 
18
- // Component rows: bars grow left → right from the 0 guide; the total card sits
19
- // to the right of the chart.
20
- const ROW = { top: 84, pitch: 44, height: 18 };
21
- const AXIS = { zero: 162, unit: 150, max: 3 }; // 0 at x=162, 3 at x=612
22
- const BASELINE = 13.2; // 14 px text on an 18 px row
19
+ // Component rows keep the design's 40 px pitch and row mid-line (top + 8); the
20
+ // bar is 22 px, 3 px taller than the design's 16 px on each side.
21
+ const ROW = { top: 54, pitch: 40, height: 22, inset: -3 };
22
+ const AXIS = { zero: 174, unit: 400 / 3, max: 3, gap: 6 }; // 0 at x=174, 3 at x=574; score 6 px inside the tip
23
+ const BASELINE = 12.06; // 13 px text on the row mid-line, from the row top
23
24
 
24
- // Total card bar, also left → right: 0 at the card's left inset, 21 at its right.
25
- const TOTAL = { left: 713, length: 130, max: 21, cutoff: 6 };
25
+ // Total column: vertical bar filling bottom → up, 0 at y=316, 21 at y=46.
26
+ const TOTAL = { bottom: 316, length: 270, max: 21, cutoff: 6 };
26
27
 
27
28
  // Interpretation panel: one fixed line pitch; the conditional false-positive
28
29
  // line collapses its space when the screen is negative.
@@ -55,15 +56,15 @@ class PSQI93 extends Profile {
55
56
  profile: {
56
57
  get dimensions() {
57
58
  return {
58
- width: CHART.width + 2 * this.padding.x, // 863 → 903
59
- height: CHART.height + 2 * this.padding.y, // 674 → 714
59
+ width: CHART.width + 2 * this.padding.x, // 789 → 903
60
+ height: CHART.height + 2 * this.padding.y, // 334 → 714
60
61
  };
61
62
  },
62
63
  // Chart inset inside the 943 × 754 design page minus the 20 px the layout
63
64
  // owns on every side, so the with-sidebar page renders at scale 1.
64
65
  padding: {
65
- x: (943 - CHART.width) / 2 - 20, // 20
66
- y: (754 - CHART.height) / 2 - 20, // 20
66
+ x: (943 - CHART.width) / 2 - 20, // 57
67
+ y: (754 - CHART.height) / 2 - 20, // 190
67
68
  },
68
69
  },
69
70
  labels: Object.values(this.labels),
@@ -90,15 +91,18 @@ class PSQI93 extends Profile {
90
91
  title: data.label.title,
91
92
  mark,
92
93
  top,
94
+ barTop: top + ROW.inset,
93
95
  width,
94
96
  x: AXIS.zero,
97
+ labelX: AXIS.zero + width - AXIS.gap,
95
98
  baseline: top + BASELINE,
96
99
  };
97
100
  });
98
101
 
99
102
  const totalMark = s[7].mark ?? 0;
100
103
  const positive = totalMark >= TOTAL.cutoff;
101
- const totalWidth = (TOTAL.length * totalMark) / TOTAL.max;
104
+ const totalHeight = (TOTAL.length * totalMark) / TOTAL.max;
105
+ const totalTip = TOTAL.bottom - totalHeight;
102
106
 
103
107
  const line = (n) => TEXT.top + n * TEXT.pitch;
104
108
  const falsePositive = positive ? line(3) : null;
@@ -112,8 +116,12 @@ class PSQI93 extends Profile {
112
116
  total: {
113
117
  mark: totalMark,
114
118
  max: TOTAL.max,
115
- width: totalWidth,
116
- x: TOTAL.left,
119
+ height: totalHeight,
120
+ tip: totalTip,
121
+ // 18 px number centred on the tip of the bar, travelling with it.
122
+ baseline: totalTip + 6.5,
123
+ // Screening cut: a total of exactly 6 puts the tip on this line.
124
+ cut: TOTAL.bottom - (TOTAL.length * TOTAL.cutoff) / TOTAL.max,
117
125
  },
118
126
  screen: {
119
127
  positive,
@@ -1,107 +1,144 @@
1
1
  {{#> layout}}
2
2
 
3
3
  <defs>
4
- {{! Component bar: grows left → right, so the gradient runs from the 0 guide (light)
5
- to the tip (dark) in each bar's own bounding box. One hue for all seven — a
6
- per-component colour would read as a class the scoring does not define. }}
7
- <linearGradient id="psqiBar" x1="0" y1="0" x2="1" y2="0">
4
+ {{! Component bars, alternating indigo / violet by row: Figma anchors the gradient to
5
+ each bar's own width, so bounding-box units reproduce every bar from one definition. }}
6
+ <linearGradient id="psqiBarIndigo" x1="0" y1="0" x2="1" y2="0">
8
7
  <stop stop-color="#A5B4FC"/>
9
8
  <stop offset="1" stop-color="#4338CA"/>
10
9
  </linearGradient>
10
+ <linearGradient id="psqiBarViolet" x1="0" y1="0" x2="1" y2="0">
11
+ <stop stop-color="#C4B5FD"/>
12
+ <stop offset="1" stop-color="#6D28D9"/>
13
+ </linearGradient>
14
+
15
+ {{! Total bar: the gradient runs 194 % of the fill height up from the foot of the bar,
16
+ so the visible tip keeps the same tone at every score. }}
17
+ <linearGradient id="psqiTotal" x1="0" y1="1" x2="0" y2="-0.94">
18
+ <stop stop-color="#94A3B8"/>
19
+ <stop offset="1" stop-color="#334155"/>
20
+ </linearGradient>
21
+
22
+ {{! Same run in red, for a total at or above the screening cut }}
23
+ <linearGradient id="psqiTotalPositive" x1="0" y1="1" x2="0" y2="-0.94">
24
+ <stop stop-color="#FCA5A5"/>
25
+ <stop offset="1" stop-color="#B91C1C"/>
26
+ </linearGradient>
11
27
 
12
- {{! Total track, fully rounded; the fill is clipped to it so any width keeps the ends. }}
13
- <clipPath id="psqiTotalTrack">
14
- <rect x="713" y="260" width="130" height="10" rx="5"/>
15
- </clipPath>
28
+ {{! Shadow of the white 0 baseline (offset right, soft blur) }}
29
+ <filter id="psqiBaseShadow" x="171" y="27" width="7" height="310" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
30
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
31
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
32
+ <feOffset dx="1"/>
33
+ <feGaussianBlur stdDeviation="1.5"/>
34
+ <feComposite in2="hardAlpha" operator="out"/>
35
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
36
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1"/>
37
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1" result="shape"/>
38
+ </filter>
39
+
40
+ {{! Same treatment under the foot of the total bar }}
41
+ <filter id="psqiTotalBaseShadow" x="711" y="310" width="34" height="14" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
42
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
43
+ <feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
44
+ <feOffset dx="2"/>
45
+ <feGaussianBlur stdDeviation="3"/>
46
+ <feComposite in2="hardAlpha" operator="out"/>
47
+ <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
48
+ <feBlend mode="normal" in2="BackgroundImageFix" result="effect1"/>
49
+ <feBlend mode="normal" in="SourceGraphic" in2="effect1" result="shape"/>
50
+ </filter>
16
51
  </defs>
17
52
 
18
53
  <g transform="translate({{spec.profile.padding.x}}, {{spec.profile.padding.y}})">
19
54
 
20
55
  {{#if valid}}
21
56
 
22
- {{! ===================== Components: heading (over the chart, right-aligned to it) ===================== }}
23
- <text x="660" y="24" text-anchor="start" font-size="16" font-weight="600" fill="#1E293B">مولفه‌های کیفیت خواب</text>
24
- <text x="660" y="46" text-anchor="start" font-size="12" fill="#64748B">نمره هر مولفه از 0 تا 3؛ نمره بالاتر یعنی مشکل بیشتر</text>
25
-
26
- {{! Axis values over the 0–3 guides, and the raw-score column header }}
57
+ {{! ===================== Gridlines (1, 2, 3) ===================== }}
27
58
  {{#each ticks as |tick|}}
28
- <text x="{{tick.x}}" y="72" text-anchor="middle" font-size="12" fill="#94A3B8">{{tick.v}}</text>
59
+ {{#if tick.v}}
60
+ <line x1="{{math tick.x '-' 0.5}}" y1="30.5" x2="{{math tick.x '-' 0.5}}" y2="333.5" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
61
+ {{/if}}
29
62
  {{/each}}
30
- <text x="644" y="72" text-anchor="middle" font-size="12" fill="#94A3B8">نمره</text>
31
63
 
32
- {{! ===================== Gridlines (1, 2, 3) ===================== }}
33
- <line x1="312.5" y1="78" x2="312.5" y2="366" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
34
- <line x1="462.5" y1="78" x2="462.5" y2="366" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
35
- <line x1="612.5" y1="78" x2="612.5" y2="366" stroke="#E2E8F0" stroke-linecap="round" stroke-dasharray="6 6"/>
36
-
37
- {{! ===================== Tracks ===================== }}
64
+ {{! ===================== Bar tracks ===================== }}
38
65
  {{#each items as |item|}}
39
- <g transform="translate(162, {{item.top}})">
40
- {{bar 450 18 (object tl=0 bl=0 tr=3 br=3) (toRad 0) fill="black" fill-opacity="0.04"}}
66
+ <g transform="translate(174, {{item.barTop}})">
67
+ {{bar 400 22 (object tl=0 bl=0 tr=3 br=3) (toRad 0) fill="black" fill-opacity="0.04"}}
41
68
  </g>
42
69
  {{/each}}
43
70
 
44
- {{! ===================== Bars ===================== }}
71
+ {{! ===================== Bars + score inside the tip (nothing for 0) ===================== }}
45
72
  {{#each items as |item|}}
46
73
  {{#if item.width}}
47
- <g transform="translate({{item.x}}, {{item.top}})">
48
- {{bar item.width 18 (object tl=0 bl=0 tr=3 br=3) (toRad 0) fill="url(#psqiBar)"}}
74
+ <g transform="translate(174, {{item.barTop}})">
75
+ {{bar item.width 22 (object tl=0 bl=0 tr=3 br=3) (toRad 0) fill=(ternary (math @index '%' 2) "url(#psqiBarViolet)" "url(#psqiBarIndigo)")}}
49
76
  </g>
77
+ {{! 15 px number centred on the row mid-line (top + 8) }}
78
+ <text x="{{item.labelX}}" y="{{math item.baseline '+' 0.59}}" text-anchor="end" direction="ltr" font-size="15" font-weight="600" fill="white">{{item.mark}}</text>
50
79
  {{/if}}
51
80
  {{/each}}
52
81
 
53
- {{! 0 guide }}
54
- <line x1="161.5" y1="78" x2="161.5" y2="366" stroke="#CBD5E1" stroke-linecap="round"/>
55
-
56
- {{! ===================== Component names (left of the 0 guide) and raw scores (after 3) ===================== }}
82
+ {{! ===================== Component names ===================== }}
57
83
  {{#each items as |item|}}
58
- <text x="150" y="{{item.baseline}}" text-anchor="start" font-size="14" fill="#334155">{{item.title}}</text>
59
- <rect x="628.5" y="{{math item.top '+' 0.5}}" width="31" height="17" rx="4" fill="white" stroke="#6366F1"/>
60
- <text x="644" y="{{item.baseline}}" text-anchor="middle" font-size="13" font-weight="600" fill="#3730A3">{{item.mark}}</text>
84
+ <text x="165" y="{{item.baseline}}" text-anchor="start" font-size="13" fill="#475569">{{item.title}}</text>
61
85
  {{/each}}
62
86
 
63
- {{! ===================== Total card (right of the chart) ===================== }}
64
- <rect x="693.5" y="78.5" width="169" height="287" rx="8" fill="#F8FAFC" stroke="#E2E8F0"/>
65
- <text x="778" y="136" text-anchor="middle" font-size="14" font-weight="600" fill="#475569">نمره کل</text>
66
- <text x="778" y="206" text-anchor="middle" font-size="48" font-weight="700" fill="#1E293B">{{total.mark}}</text>
67
- <text x="778" y="232" text-anchor="middle" font-size="13" fill="#64748B">از {{total.max}}</text>
87
+ {{! ===================== White shadowed 0 baseline ===================== }}
88
+ <g opacity="0.5" filter="url(#psqiBaseShadow)">
89
+ <line x1="173.5" y1="30.5" x2="173.5" y2="333.5" stroke="white" stroke-linecap="round"/>
90
+ </g>
91
+
92
+ {{! ===================== Axis labels, centred on their guides ===================== }}
93
+ {{#each ticks as |tick|}}
94
+ <text x="{{math tick.x '-' 0.5}}" y="18.06" text-anchor="middle" font-size="12" fill="#94A3B8">{{tick.v}}</text>
95
+ {{/each}}
68
96
 
69
- {{! Neutral 0–21 bar, filling left → right; deliberately no cut mark or colour change }}
70
- <rect x="713" y="260" width="130" height="10" rx="5" fill="#E2E8F0"/>
71
- {{#if total.width}}
72
- <rect x="{{total.x}}" y="260" width="{{total.width}}" height="10" fill="#475569" clip-path="url(#psqiTotalTrack)"/>
97
+ {{! ===================== Total score column ===================== }}
98
+ <g transform="translate(717, 46)">
99
+ {{bar 18 270 (object tl=2 tr=2 bl=0 br=0) (toRad 0) fill="#F1F5F9"}}
100
+ </g>
101
+ {{! Track above the cut tinted red: the 6–21 zone }}
102
+ <g transform="translate(717, 46)">
103
+ {{bar 18 (math total.cut '-' 46) (object tl=2 tr=2 bl=0 br=0) (toRad 0) fill="#FEF2F2"}}
104
+ </g>
105
+ {{#if total.height}}
106
+ <g transform="translate(717, {{total.tip}})">
107
+ {{bar 18 total.height (object tl=2 tr=2 bl=0 br=0) (toRad 0) fill=(ternary screen.positive "url(#psqiTotalPositive)" "url(#psqiTotal)")}}
108
+ </g>
73
109
  {{/if}}
74
- <text x="713" y="288" text-anchor="middle" font-size="11" fill="#94A3B8">0</text>
75
- <text x="843" y="288" text-anchor="middle" font-size="11" fill="#94A3B8">21</text>
76
- <text x="778" y="322" text-anchor="middle" font-size="12" fill="#94A3B8">جمع هفت مولفه</text>
77
110
 
78
- {{! ===================== Interpretation panel ===================== }}
79
- {{! Every sentence here is approved fixed text; the screening result appears nowhere else. }}
80
- <rect x="0.5" y="410.5" width="862" height="263" rx="8" fill="#F8FAFC" stroke="#E2E8F0"/>
81
- <text x="843" y="452" text-anchor="start" font-size="15" font-weight="600" fill="#1E293B">تفسیر نتیجه</text>
111
+ {{! Screening cut on score 6, drawn over the fill }}
112
+ <line x1="715" y1="{{total.cut}}" x2="737" y2="{{total.cut}}" stroke="#DC2626" stroke-width="1.5" stroke-linecap="round" stroke-dasharray="3 3"/>
82
113
 
83
- <text x="843" y="{{lines.direction}}" text-anchor="start" font-size="13.5" fill="#334155"><tspan font-weight="600">جهت نمره</tspan>: نمره بالاتر نشان‌دهنده مشکلات بیشتر در ابعاد سنجیده‌شده است.</text>
114
+ {{! Cut value, same row family as the 21 label, 2 px further from the column: right edge on x=709 }}
115
+ <text x="709" y="{{math total.cut '+' 3.06}}" text-anchor="end" direction="ltr" font-size="12" fill="#475569">6</text>
84
116
 
85
- <text x="843" y="{{lines.screen}}" text-anchor="start" font-size="13.5" fill="#334155"><tspan font-weight="600">نتیجه غربال (غربال، نه تشخیص)</tspan>: نمره کل {{total.mark}} از {{total.max}} — <tspan font-weight="600">{{screen.text}}</tspan>.</text>
117
+ {{! Red-zone label, rotated like the column title and centred between the cut and the top (21) }}
118
+ <text x="0" y="0" transform="translate(705.02, {{math (math total.cut '+' 46) '/' 2}}) rotate(-90)" text-anchor="middle" font-size="12" font-weight="500" fill="#DC2626">نیازمند بررسی بیشتر</text>
86
119
 
87
- <text x="843" y="{{lines.cutoff}}" text-anchor="start" font-size="13.5" fill="#334155"><tspan font-weight="600">برش غربال</tspan>: نمره کل 6 یا بیشتر؛ برش فقط بر نمره کل اعمال می‌شود و مولفه‌ها طبقه‌بندی نمی‌شوند.</text>
120
+ {{! Total score travelling with the tip of the bar }}
121
+ <text x="743" y="{{total.baseline}}" text-anchor="start" direction="ltr" font-size="18" font-weight="600" fill="{{ternary screen.positive '#B91C1C' '#334155'}}">{{total.mark}}</text>
88
122
 
89
- {{#if screen.positive}}
90
- <text x="843" y="{{lines.falsePositive}}" text-anchor="start" font-size="13.5" fill="#334155"><tspan font-weight="600">مثبت کاذب</tspan>: ویژگی این برش 72.2 درصد است؛ بخشی از افراد بالای برش در واقع مشکل ندارند و نمره زیر برش اطلاع‌بخش‌تر است.</text>
91
- {{/if}}
123
+ {{! Column title, bottom end on the foot of the bar }}
124
+ <text x="0" y="0" transform="translate(705.02, 317.2) rotate(-90)" text-anchor="end" font-size="12" fill="#475569">نمره کل</text>
125
+
126
+ {{! Foot of the total bar }}
127
+ <g opacity="0.5" filter="url(#psqiTotalBaseShadow)">
128
+ <path d="M716 317H736" stroke="white" stroke-width="2" stroke-linecap="round"/>
129
+ </g>
92
130
 
93
- <text x="843" y="{{lines.limits1}}" text-anchor="start" font-size="13.5" fill="#334155"><tspan font-weight="600">محدودیت</tspan>: این نتیجه بر پایه گزارش خود فرد درباره ماه گذشته است و تشخیص نیست؛ ابزار تصمیم‌یار است</text>
94
- <text x="843" y="{{lines.limits2}}" text-anchor="start" font-size="13.5" fill="#334155">و جای مصاحبه بالینی و اندازه‌گیری عینی خواب را نمی‌گیرد.</text>
131
+ <text x="711" y="49.06" text-anchor="end" direction="ltr" font-size="12" fill="#475569">{{total.max}}</text>
95
132
 
96
133
  {{else}}
97
134
 
98
135
  {{! ===================== No result (all-or-nothing) ===================== }}
99
- <rect x="181.5" y="237.5" width="500" height="199" rx="12" fill="#F8FAFC" stroke="#E2E8F0"/>
100
- <circle cx="431.5" cy="288" r="17" fill="none" stroke="#64748B" stroke-width="2"/>
101
- <line x1="431.5" y1="279" x2="431.5" y2="291" stroke="#64748B" stroke-width="2.5" stroke-linecap="round"/>
102
- <circle cx="431.5" cy="297.5" r="1.6" fill="#64748B"/>
103
- <text x="431.5" y="344" text-anchor="middle" font-size="17" font-weight="600" fill="#1E293B">نتیجه تولید نشد؛ پاسخ ناقص یا نامعتبر است</text>
104
- <text x="431.5" y="376" text-anchor="middle" font-size="13" fill="#64748B">برای این پاسخ‌نامه هیچ نمره‌ای، حتی نمره جزئی، گزارش نمی‌شود.</text>
136
+ <rect x="144.5" y="67.5" width="500" height="199" rx="12" fill="#F8FAFC" stroke="#E2E8F0"/>
137
+ <circle cx="394.5" cy="118" r="17" fill="none" stroke="#64748B" stroke-width="2"/>
138
+ <line x1="394.5" y1="109" x2="394.5" y2="121" stroke="#64748B" stroke-width="2.5" stroke-linecap="round"/>
139
+ <circle cx="394.5" cy="127.5" r="1.6" fill="#64748B"/>
140
+ <text x="394.5" y="174" text-anchor="middle" font-size="17" font-weight="600" fill="#1E293B">نتیجه تولید نشد؛ پاسخ ناقص یا نامعتبر است</text>
141
+ <text x="394.5" y="206" text-anchor="middle" font-size="13" fill="#64748B">برای این پاسخ‌نامه هیچ نمره‌ای، حتی نمره جزئی، گزارش نمی‌شود.</text>
105
142
 
106
143
  {{/if}}
107
144