OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
@@ -0,0 +1,537 @@
|
||||
{{--
|
||||
Stakeholder Report — Page 4 (Impact Journey)
|
||||
|
||||
"Not a tracker — a vision that becomes proof."
|
||||
|
||||
One artifact, three lens views, tense-driven. Same bars, targets, and
|
||||
people stay on screen; the framing and tense change around them.
|
||||
|
||||
Vision (Day 1, always available) — targets only, "will"
|
||||
Progress (unlocks with any snapshot) — captured + remaining, "is becoming"
|
||||
Impact (unlocks when targets met) — achieved, "did"
|
||||
|
||||
Bookends both authored, both present Day 1:
|
||||
STARTED (the world / the problem) → the journey → DIFFERENT (the impact)
|
||||
|
||||
Human meaning is AUTHORED on the canvas (item.description / item.assumptions
|
||||
/ item.conclusion), never generated — §7 rule 12. A concatenated narrative
|
||||
is a fabrication when this ends up in a funder's hands.
|
||||
|
||||
Per-lens content is rendered server-side and CSS-toggled by the parent
|
||||
wrapper's data-active-lens attribute — so the "same component, words &
|
||||
fill change" reads as one journey maturing, not hard cuts.
|
||||
|
||||
Vars in:
|
||||
$logicModel null | {narrative, coverageMatrix, projectLinks, linkedGoals, ...}
|
||||
$hasLM bool
|
||||
$scope 'strategy' | 'program'
|
||||
--}}
|
||||
|
||||
|
||||
@if (! $hasLM)
|
||||
<div class="p4-wrap">
|
||||
<div class="p4-empty">
|
||||
<div class="lb">{{ __('stakeholder.ij.no_lm_title') }}</div>
|
||||
<div class="s">{{ __('stakeholder.ij.no_lm_hint') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
@php
|
||||
$stages = $logicModel['coverageMatrix']['stages'] ?? [];
|
||||
$projectLinks = $logicModel['projectLinks'] ?? [];
|
||||
$linkedGoals = $logicModel['linkedGoals'] ?? [];
|
||||
|
||||
$outputItems = $stages['outputs']['items'] ?? [];
|
||||
$outcomeItems = $stages['outcomes']['items'] ?? [];
|
||||
$impactItems = $stages['impact']['items'] ?? [];
|
||||
|
||||
// Metric aggregator (unchanged from before; comment retained for context).
|
||||
$metricFor = function ($item) use ($projectLinks, $linkedGoals) {
|
||||
$itemId = (int) (((array) $item)['id'] ?? 0);
|
||||
$links = $projectLinks[$itemId] ?? [];
|
||||
$goals = [];
|
||||
foreach ($links as $link) {
|
||||
if (($link['linked_entity_type'] ?? '') !== 'goal') continue;
|
||||
$gid = (int) ($link['linked_entity_id'] ?? 0);
|
||||
if (isset($linkedGoals[$gid])) $goals[] = $linkedGoals[$gid];
|
||||
}
|
||||
if (count($goals) === 0) return null;
|
||||
|
||||
$current = array_sum(array_column($goals, 'currentValue'));
|
||||
$target = array_sum(array_column($goals, 'endValue'));
|
||||
$unit = $goals[0]['metricType'] ?? 'number';
|
||||
|
||||
$byDate = [];
|
||||
foreach ($goals as $g) {
|
||||
foreach (($g['snapshots'] ?? []) as $s) {
|
||||
$day = substr((string) $s['date'], 0, 10);
|
||||
if (! isset($byDate[$day])) $byDate[$day] = 0.0;
|
||||
$byDate[$day] += (float) $s['value'];
|
||||
}
|
||||
}
|
||||
ksort($byDate);
|
||||
$snapshots = [];
|
||||
foreach ($byDate as $day => $value) $snapshots[] = ['date' => $day, 'value' => $value];
|
||||
|
||||
$arr = (array) $item;
|
||||
// Meaning source per §1 of the authored-meaning spec:
|
||||
// why_this_matters — the primary source (authored, nullable)
|
||||
// No fallback to conclusion — conclusion narrows to "as measured
|
||||
// by" methodology only, going forward. Mining it for meaning is
|
||||
// exactly the double-duty that broke Page 2 earlier.
|
||||
return [
|
||||
'id' => $itemId,
|
||||
'label' => trim((string) ($arr['description'] ?? '')),
|
||||
'meaning' => trim((string) ($arr['why_this_matters'] ?? '')),
|
||||
'measuredBy' => trim((string) ($arr['conclusion'] ?? '')),
|
||||
'current' => $current,
|
||||
'target' => $target,
|
||||
'unit' => $unit,
|
||||
'snapshots' => $snapshots,
|
||||
];
|
||||
};
|
||||
|
||||
$anySnapshots = false;
|
||||
$collectMetrics = function (array $items) use ($metricFor, &$anySnapshots) {
|
||||
$metrics = [];
|
||||
foreach ($items as $it) {
|
||||
$m = $metricFor($it);
|
||||
if ($m === null) continue;
|
||||
if (count($m['snapshots']) > 0) $anySnapshots = true;
|
||||
$metrics[] = $m;
|
||||
}
|
||||
return $metrics;
|
||||
};
|
||||
$outMetrics = $collectMetrics($outputItems);
|
||||
$ocMetrics = $collectMetrics($outcomeItems);
|
||||
|
||||
$anyHit = false;
|
||||
foreach (array_merge($outMetrics, $ocMetrics) as $m) {
|
||||
if ($m['target'] > 0 && $m['current'] >= $m['target']) { $anyHit = true; break; }
|
||||
}
|
||||
$progressUnlocked = $anySnapshots;
|
||||
$impactUnlocked = $anyHit;
|
||||
|
||||
// Default lens = the most advanced state the data supports. A page
|
||||
// with snapshots defaults to Progress (not Vision) so a returning
|
||||
// reader lands on the current reality — Vision is reachable via
|
||||
// the toggle for the "share the promise" flow.
|
||||
$defaultLens = $impactUnlocked ? 'impact' : ($progressUnlocked ? 'progress' : 'vision');
|
||||
|
||||
$fmt = function ($value, string $unit) {
|
||||
$value = (float) $value;
|
||||
if ($unit === 'percent') return rtrim(rtrim(number_format($value, 1, '.', ''), '0'), '.').'%';
|
||||
if ($value == floor($value)) return number_format($value, 0, '.', ',');
|
||||
return number_format($value, 1, '.', ',');
|
||||
};
|
||||
|
||||
// Detects whether the authored label starts with the target number
|
||||
// (e.g. "1,200 screenings completed" for target 1,200). Used to
|
||||
// suppress the redundant "target N" subtitle on Vision — the label
|
||||
// is already the promise.
|
||||
$labelContainsTarget = function (string $label, float $target, string $unit) use ($fmt): bool {
|
||||
if ($target <= 0) return false;
|
||||
$formatted = $fmt($target, $unit);
|
||||
// strip commas for a looser match too
|
||||
$stripped = str_replace(',', '', $formatted);
|
||||
$labelStripped = str_replace(',', '', $label);
|
||||
return stripos($labelStripped, $stripped) !== false;
|
||||
};
|
||||
|
||||
// ── STARTED bookend text.
|
||||
// Authored ONLY. Sourced from the Impact item's `starting_picture`
|
||||
// (a dedicated field for the world today, before this work). No
|
||||
// synthesis, no fallback to narrative concatenation — an empty state
|
||||
// is more honest than a fabrication, and this artifact ends up in
|
||||
// funders' hands.
|
||||
$startedText = '';
|
||||
if (count($impactItems) > 0) {
|
||||
$startedText = trim((string) (((array) $impactItems[0])['starting_picture'] ?? ''));
|
||||
}
|
||||
|
||||
// ── DIFFERENT bookend: authored impact title + authored meaning.
|
||||
// Meaning source: why_this_matters ONLY. No fallback to conclusion
|
||||
// (which is now "as measured by" methodology). No fallback to
|
||||
// assumptions (that's for the theory-of-change assertion, a
|
||||
// different concept from the funder-facing meaning).
|
||||
$differentTitle = '';
|
||||
$differentMeaning = '';
|
||||
if (count($impactItems) > 0) {
|
||||
$impArr = (array) $impactItems[0];
|
||||
$differentTitle = trim((string) ($impArr['description'] ?? ''));
|
||||
$differentMeaning = trim((string) ($impArr['why_this_matters'] ?? ''));
|
||||
}
|
||||
|
||||
// ── Arc statement (Vision beat 2). The ONE place a light
|
||||
// concatenation is correct: assembled from authored labels, capped
|
||||
// at 3 producing + 2 achieving, two sentences maximum. Never the
|
||||
// §8 rule-1 canvas dump. Nothing generated — every word is a label
|
||||
// a human wrote on the canvas.
|
||||
$capProducing = array_slice($outMetrics, 0, 3);
|
||||
$capAchieving = array_slice($ocMetrics, 0, 2);
|
||||
$producingLabels = array_values(array_filter(array_map(
|
||||
static fn ($m) => trim((string) $m['label']),
|
||||
$capProducing
|
||||
)));
|
||||
$achievingLabels = array_values(array_filter(array_map(
|
||||
static fn ($m) => trim((string) $m['label']),
|
||||
$capAchieving
|
||||
)));
|
||||
|
||||
$producingSentence = $producingLabels === []
|
||||
? ''
|
||||
: implode('. ', $producingLabels).'.';
|
||||
|
||||
$achievingSentence = '';
|
||||
if (count($achievingLabels) === 1) {
|
||||
$achievingSentence = sprintf(__('stakeholder.ij.beat_arc_leading_to'), $achievingLabels[0]).'.';
|
||||
} elseif (count($achievingLabels) >= 2) {
|
||||
// Natural join: "X and Y" for two, "X, Y, and Z" for three (only
|
||||
// if the cap is ever raised).
|
||||
if (count($achievingLabels) === 2) {
|
||||
$joined = $achievingLabels[0].' '.__('stakeholder.ij.beat_arc_and').' '.$achievingLabels[1];
|
||||
} else {
|
||||
$joined = implode(', ', array_slice($achievingLabels, 0, -1))
|
||||
.', '.__('stakeholder.ij.beat_arc_and').' '.end($achievingLabels);
|
||||
}
|
||||
$achievingSentence = sprintf(__('stakeholder.ij.beat_arc_leading_to'), $joined).'.';
|
||||
}
|
||||
|
||||
$arcStatement = trim($producingSentence.' '.$achievingSentence);
|
||||
|
||||
// Only the "Logic Model canvas" phrase links out — the surrounding nudge
|
||||
// sentence stays plain text. Built once, injected via sprintf %s below.
|
||||
$lmCanvasLink = '<a href="'.BASE_URL.'/logicmodelcanvas/showCanvas">'.e(__('stakeholder.ij.nudge_link')).'</a>';
|
||||
@endphp
|
||||
|
||||
<div class="p4-wrap" data-active-lens="{{ $defaultLens }}" data-p4-lens-wrap>
|
||||
|
||||
{{-- Header --}}
|
||||
<div class="p4-hd">
|
||||
<div>
|
||||
<div class="t">{{ __('stakeholder.ij.header_title') }}</div>
|
||||
<div class="s">{{ __('stakeholder.ij.header_sub') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lens toggle — shows only when >=2 lenses exist for the reader --}}
|
||||
@if ($progressUnlocked || $impactUnlocked)
|
||||
<div class="p4-lens" data-p4-lens>
|
||||
<button type="button" class="lopt @if ($defaultLens === 'vision') is-active @endif" data-lens-target="vision">{{ __('stakeholder.ij.lens_vision') }}</button>
|
||||
<button type="button" class="lopt @if ($defaultLens === 'progress') is-active @endif @if (! $progressUnlocked) locked @endif" data-lens-target="progress" @if (! $progressUnlocked) disabled @endif>
|
||||
@if (! $progressUnlocked)<i class="fa fa-lock"></i>@endif
|
||||
{{ __('stakeholder.ij.lens_progress') }}
|
||||
</button>
|
||||
<button type="button" class="lopt @if ($defaultLens === 'impact') is-active @endif @if (! $impactUnlocked) locked @endif" data-lens-target="impact" @if (! $impactUnlocked) disabled @endif>
|
||||
@if (! $impactUnlocked)<i class="fa fa-lock"></i>@endif
|
||||
{{ __('stakeholder.ij.lens_impact') }}
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- ── VISION lens: three beats. Complete on day one, by design.
|
||||
Beat 1 (world today) and beat 3 (world delivered) carry the
|
||||
same authored text as the STARTED/DIFFERENT bookends below
|
||||
— the block-level lens toggle means only one shows at a
|
||||
time, so there is no duplication for the reader. --}}
|
||||
<div class="p4-vision" data-lens-block="vision">
|
||||
<div class="beat today">
|
||||
<div class="lb"><i class="fa fa-flag"></i> {{ __('stakeholder.ij.beat_today_lb') }}</div>
|
||||
@if ($startedText !== '')
|
||||
<div class="txt">{{ $startedText }}</div>
|
||||
@else
|
||||
<div class="empty">
|
||||
{{ __('stakeholder.ij.bookend_started_empty') }}
|
||||
<span class="nudge">{!! sprintf(e(__('stakeholder.ij.bookend_started_nudge')), $lmCanvasLink) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="beat arc">
|
||||
<div class="lb"><i class="fa fa-arrow-trend-up"></i> {{ __('stakeholder.ij.beat_arc_lb') }}</div>
|
||||
@if ($arcStatement !== '')
|
||||
<div class="statement">{{ $arcStatement }}</div>
|
||||
@else
|
||||
<div class="empty">
|
||||
{{ __('stakeholder.ij.beat_arc_empty') }}
|
||||
<span class="nudge">{!! sprintf(e(__('stakeholder.ij.beat_arc_empty_nudge')), $lmCanvasLink) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="beat delivered">
|
||||
<div class="lb"><i class="fa fa-bullseye"></i> {{ __('stakeholder.ij.beat_delivered_lb') }}</div>
|
||||
@if ($differentTitle !== '')
|
||||
<div class="txt">{{ $differentTitle }}</div>
|
||||
@if ($differentMeaning !== '')
|
||||
<div class="meaning">{{ $differentMeaning }}</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="empty">
|
||||
{{ __('stakeholder.ij.bookend_different_empty') }}
|
||||
<span class="nudge">{!! sprintf(e(__('stakeholder.ij.bookend_different_nudge')), $lmCanvasLink) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ── PROGRESS + IMPACT: the same bookends flanking the arc with
|
||||
live tracks. Hidden entirely on the Vision lens by the
|
||||
block-level toggle above. --}}
|
||||
<div data-lens-block="tracks">
|
||||
|
||||
{{-- STARTED bookend — authored problem text or honest empty state.
|
||||
NO narrative dump. NO generated summary. --}}
|
||||
<div class="p4-bookend started">
|
||||
<div class="lb"><i class="fa fa-flag"></i> {{ __('stakeholder.ij.bookend_started') }}</div>
|
||||
@if ($startedText !== '')
|
||||
<div class="meaning" style="margin-top:0;">{{ $startedText }}</div>
|
||||
@else
|
||||
<div class="empty">
|
||||
{{ __('stakeholder.ij.bookend_started_empty') }}
|
||||
<span class="nudge">{!! sprintf(e(__('stakeholder.ij.bookend_started_nudge')), $lmCanvasLink) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- The Arc --}}
|
||||
@if (count($outMetrics) > 0 || count($ocMetrics) > 0)
|
||||
<div class="p4-arc">
|
||||
<div class="lb"><i class="fa fa-arrow-trend-up"></i> {{ __('stakeholder.ij.arc_label') }}</div>
|
||||
|
||||
@foreach ([
|
||||
['key'=>'outputs', 'items'=>$outMetrics, 'title'=>__('stakeholder.ij.arc_producing'), 'icon'=>'fa-boxes-stacked'],
|
||||
['key'=>'outcomes', 'items'=>$ocMetrics, 'title'=>__('stakeholder.ij.arc_achieving'), 'icon'=>'fa-chart-line'],
|
||||
] as $group)
|
||||
@if (count($group['items']) === 0) @continue @endif
|
||||
<div class="p4-mgroup {{ $group['key'] }}">
|
||||
<div class="gh"><i class="fa {{ $group['icon'] }}"></i> {{ $group['title'] }}</div>
|
||||
|
||||
@foreach ($group['items'] as $m)
|
||||
@php
|
||||
$n = count($m['snapshots']);
|
||||
$growthState = $n <= 1 ? 'baseline' : ($n === 2 ? 'before_after' : 'full_arc');
|
||||
$hitTarget = $m['target'] > 0 && $m['current'] >= $m['target'];
|
||||
$peak = max($m['target'], $m['current'], 1);
|
||||
foreach ($m['snapshots'] as $s) $peak = max($peak, (float) $s['value']);
|
||||
$barH = fn ($v) => max(4, min(50, (int) round(($v / $peak) * 50)));
|
||||
$labelHasTarget = $labelContainsTarget($m['label'], $m['target'], $m['unit']);
|
||||
@endphp
|
||||
<div class="p4-metric">
|
||||
<div class="mn">
|
||||
@if ($m['meaning'] !== '')
|
||||
{{-- Meaning leads. The metric is evidence.
|
||||
"as measured by {label}" reads as the receipt
|
||||
underneath — same rule as Page 2's verdict + read
|
||||
pattern, one level down. --}}
|
||||
{{ $m['meaning'] }}
|
||||
<span class="meaning">
|
||||
{{ __('stakeholder.ij.as_measured_by') }} {{ $m['label'] }}
|
||||
@if ($m['measuredBy'] !== '')
|
||||
— {{ $m['measuredBy'] }}
|
||||
@endif
|
||||
</span>
|
||||
@else
|
||||
{{-- No authored meaning yet: today's behavior — the
|
||||
authored label leads, optional methodology below.
|
||||
No regression. --}}
|
||||
{{ $m['label'] }}
|
||||
@if (! $labelHasTarget && $m['target'] > 0)
|
||||
<span class="tgt">{{ __('stakeholder.ij.target_lbl') }} {{ $fmt($m['target'], $m['unit']) }}</span>
|
||||
@endif
|
||||
@if ($m['measuredBy'] !== '')
|
||||
<span class="meaning">{{ __('stakeholder.ij.as_measured_by') }} {{ $m['measuredBy'] }}</span>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- ONE persistent bar per metric. Track = the target
|
||||
(always visible, same shape all lenses). Fill = current
|
||||
progress, animates on lens change via CSS transition.
|
||||
Vision → fill 0%. Progress → fill current/target. Impact
|
||||
→ fill 100%. The morph IS the story. --}}
|
||||
@php
|
||||
// Cap the visual fill % — over-target still reads as full,
|
||||
// "we did this" carries the over-delivery in the value label.
|
||||
$progressPct = $m['target'] > 0
|
||||
? min(100, ($m['current'] / $m['target']) * 100)
|
||||
: 0;
|
||||
// Fill values per lens — the JS uses these dataset attrs
|
||||
// to swap width + label text on lens change.
|
||||
$visionFill = 0;
|
||||
$progressFill = $progressPct;
|
||||
$impactFill = 100;
|
||||
$visionLabel = __('stakeholder.ij.fill_lbl_vision');
|
||||
$progressLabel = sprintf(__('stakeholder.ij.fill_lbl_progress'), $fmt($m['current'], $m['unit']));
|
||||
$impactLabel = sprintf(__('stakeholder.ij.fill_lbl_impact'), $fmt(max($m['current'], $m['target']), $m['unit']));
|
||||
@endphp
|
||||
<div class="p4-arc-viz"
|
||||
data-p4-bar
|
||||
data-vision-fill="{{ $visionFill }}"
|
||||
data-progress-fill="{{ $progressFill }}"
|
||||
data-impact-fill="{{ $impactFill }}"
|
||||
data-vision-lbl="{{ $visionLabel }}"
|
||||
data-progress-lbl="{{ $progressLabel }}"
|
||||
data-impact-lbl="{{ $impactLabel }}">
|
||||
<div class="p4-scale">
|
||||
<span class="p4-fill-lbl">{{
|
||||
$defaultLens === 'vision' ? $visionLabel
|
||||
: ($defaultLens === 'impact' ? $impactLabel : $progressLabel)
|
||||
}}</span>
|
||||
<span class="p4-target-lbl">{{ __('stakeholder.ij.target_lbl') }} {{ $fmt($m['target'], $m['unit']) }}</span>
|
||||
</div>
|
||||
<div class="p4-track">
|
||||
<div class="p4-fill" style="width:{{
|
||||
$defaultLens === 'vision' ? $visionFill
|
||||
: ($defaultLens === 'impact' ? $impactFill : $progressFill)
|
||||
}}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- ─── VERDICTS — tense-aware, encouragement via structure ── --}}
|
||||
{{-- Vision: future tense. --}}
|
||||
<span class="p4-verdict willbe" data-lens="vision">
|
||||
<span class="sd"></span> {{ __('stakeholder.ij.v_will_be_measured') }}
|
||||
</span>
|
||||
{{-- Progress: present-becoming tense. One framing for both
|
||||
units — "N% of the way" reads truthfully for counts and
|
||||
percents alike, and stays consistent down the column
|
||||
regardless of metric type. --}}
|
||||
{{-- One progress color for every in-progress row: the text is
|
||||
uniformly "N% of the way", and the percentage itself conveys
|
||||
how far along. A silent amber↔blue flip at a hidden 75%
|
||||
threshold made identically-worded rows look different for no
|
||||
visible reason, so the dot stays consistent down the column. --}}
|
||||
<span class="p4-verdict @if ($hitTarget) hit @elseif ($n === 0) captured @else trending @endif" data-lens="progress">
|
||||
<span class="sd"></span>
|
||||
@if ($n === 0)
|
||||
{{ __('stakeholder.ij.v_no_snapshots_yet') }}
|
||||
@elseif ($hitTarget)
|
||||
{{ __('stakeholder.ij.v_hit_target_progress') }}
|
||||
@elseif ($m['target'] > 0)
|
||||
{{ sprintf(__('stakeholder.ij.v_pct_of_way'), (int) round(($m['current'] / $m['target']) * 100)) }}
|
||||
@else
|
||||
{{ __('stakeholder.ij.v_trending') }}
|
||||
@endif
|
||||
</span>
|
||||
{{-- Impact: past tense. Only truthful for hit-target rows. --}}
|
||||
<span class="p4-verdict @if ($hitTarget) hit @else captured @endif" data-lens="impact">
|
||||
<span class="sd"></span>
|
||||
@if ($hitTarget)
|
||||
{{ __('stakeholder.ij.v_we_did_this') }}
|
||||
@else
|
||||
{{ __('stakeholder.ij.v_not_yet_impact') }}
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
{{-- Structural encouragement — only when the claim can be
|
||||
reconstructed from what's on screen (§7 rule 8 / v6 Fix 2).
|
||||
Progress-lens "one more completes the arc" needed a period
|
||||
cadence the page doesn't render yet; dropped until we do. --}}
|
||||
@if (! $progressUnlocked)
|
||||
<div class="p4-chip" data-lens="vision"><i class="fa fa-circle-info"></i> {{ __('stakeholder.ij.chip_first_period') }}</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="p4-empty">
|
||||
<div class="lb">{{ __('stakeholder.ij.no_metrics_title') }}</div>
|
||||
<div class="s">{{ __('stakeholder.ij.no_metrics_hint') }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- DIFFERENT bookend — authored impact + supporting meaning.
|
||||
Meaning is the emotional payload: WHY this matters to the
|
||||
people it affects. Never generated. --}}
|
||||
<div class="p4-bookend different">
|
||||
<div class="lb"><i class="fa fa-bullseye"></i> {{ __('stakeholder.ij.bookend_different') }}</div>
|
||||
@if ($differentTitle !== '')
|
||||
<div class="txt">{{ $differentTitle }}</div>
|
||||
@if ($differentMeaning !== '')
|
||||
<div class="meaning">{{ $differentMeaning }}</div>
|
||||
@endif
|
||||
{{-- Tense hints — one per lens, framing shifts with capture state. --}}
|
||||
<span class="tense-hint" data-lens="vision">{{ __('stakeholder.ij.different_tense_vision') }}</span>
|
||||
<span class="tense-hint" data-lens="progress">{{ __('stakeholder.ij.different_tense_progress') }}</span>
|
||||
<span class="tense-hint" data-lens="impact">{{ __('stakeholder.ij.different_tense_impact') }}</span>
|
||||
@else
|
||||
<div class="empty">
|
||||
{{ __('stakeholder.ij.bookend_different_empty') }}
|
||||
<span class="nudge">{!! sprintf(e(__('stakeholder.ij.bookend_different_nudge')), $lmCanvasLink) !!}</span>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>{{-- /[data-lens-block=tracks] --}}
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var wrap = document.querySelector('[data-p4-lens-wrap]');
|
||||
var toggle = document.querySelector('[data-p4-lens]');
|
||||
if (! wrap || ! toggle) return;
|
||||
|
||||
var reducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
// Morph a single bar to the target lens: swap fill width + label text.
|
||||
// Width transition is CSS-driven; label crossfade is a quick opacity dip.
|
||||
function morphBar (viz, lens) {
|
||||
var fillEl = viz.querySelector('.p4-fill');
|
||||
var lblEl = viz.querySelector('.p4-fill-lbl');
|
||||
if (! fillEl || ! lblEl) return;
|
||||
var pct = viz.getAttribute('data-' + lens + '-fill');
|
||||
var lbl = viz.getAttribute('data-' + lens + '-lbl');
|
||||
if (pct !== null) fillEl.style.width = pct + '%';
|
||||
if (lbl !== null) {
|
||||
if (reducedMotion) {
|
||||
lblEl.textContent = lbl;
|
||||
} else {
|
||||
lblEl.style.opacity = '0';
|
||||
setTimeout(function () { lblEl.textContent = lbl; lblEl.style.opacity = '1'; }, 140);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function switchLens (lens) {
|
||||
var wasVision = wrap.getAttribute('data-active-lens') === 'vision';
|
||||
wrap.setAttribute('data-active-lens', lens);
|
||||
|
||||
// Arrival — when leaving Vision for Progress/Impact, the tracks
|
||||
// don't just appear (that would be a jump); they stagger in.
|
||||
// Rows start hidden (.arriving), then .arrived is added on a
|
||||
// stagger so the CSS transition plays. Reduced-motion → instant.
|
||||
if (wasVision && lens !== 'vision' && ! reducedMotion) {
|
||||
var rows = wrap.querySelectorAll('[data-lens-block="tracks"] .p4-metric');
|
||||
rows.forEach(function (row) { row.classList.remove('arrived'); row.classList.add('arriving'); });
|
||||
rows.forEach(function (row, i) {
|
||||
setTimeout(function () {
|
||||
row.classList.remove('arriving');
|
||||
row.classList.add('arrived');
|
||||
}, i * 80);
|
||||
});
|
||||
}
|
||||
|
||||
var bars = wrap.querySelectorAll('[data-p4-bar]');
|
||||
bars.forEach(function (viz, i) {
|
||||
// Stagger by 80ms so the morph reads as a sequence, not a jump.
|
||||
var delay = reducedMotion ? 0 : (i * 80);
|
||||
if (delay === 0) morphBar(viz, lens);
|
||||
else setTimeout(function () { morphBar(viz, lens); }, delay);
|
||||
});
|
||||
}
|
||||
|
||||
toggle.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('.lopt');
|
||||
if (! btn || btn.classList.contains('locked') || btn.hasAttribute('disabled')) return;
|
||||
var target = btn.getAttribute('data-lens-target');
|
||||
if (! target) return;
|
||||
toggle.querySelectorAll('.lopt').forEach(function (b) { b.classList.toggle('is-active', b === btn); });
|
||||
switchLens(target);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
Reference in New Issue
Block a user