@ikenga/pkg-sales 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +13 -1
- package/dist/features/sales/sales-view.js +172 -39
- package/dist/lib/bridge.js +14 -4
- package/dist/lib/create-dispatch.js +93 -0
- package/dist/lib/dispatch.js +87 -0
- package/dist/lib/facet-filter.js +41 -0
- package/dist/lib/operator.js +39 -0
- package/dist/lib/pkg-id.js +5 -0
- package/dist/lib/sales-css.js +1 -1
- package/dist/lib/ui.js +94 -24
- package/dist/sales.css +5 -0
- package/manifest.json +3 -2
- package/package.json +1 -1
package/dist/app.js
CHANGED
|
@@ -126,6 +126,12 @@ function App() {
|
|
|
126
126
|
const [bridgeReady, setBridgeReady] = useState(false);
|
|
127
127
|
const [bridgeError, setBridgeError] = useState(null);
|
|
128
128
|
const [activeFeature, setActiveFeature] = useState(null);
|
|
129
|
+
// Operator identity (hostContext.operator — see @ikenga/contract's
|
|
130
|
+
// host-context.ts). OPTIONAL: absent means unknown operator, standalone
|
|
131
|
+
// dev never sets it. Threaded down as a plain id/label pair, same shape
|
|
132
|
+
// as activeFeature, so the view fails safe instead of assuming a default.
|
|
133
|
+
const [operatorId, setOperatorId] = useState(null);
|
|
134
|
+
const [operatorLabel, setOperatorLabel] = useState(null);
|
|
129
135
|
|
|
130
136
|
useEffect(() => {
|
|
131
137
|
if (isStandalone()) {
|
|
@@ -138,11 +144,17 @@ function App() {
|
|
|
138
144
|
onContextChange: (ctx) => {
|
|
139
145
|
const af = ctx?.royaltiSuite?.activeFeature;
|
|
140
146
|
if (typeof af === 'string') setActiveFeature(af);
|
|
147
|
+
const op = ctx?.operator;
|
|
148
|
+
setOperatorId(typeof op?.id === 'string' ? op.id : null);
|
|
149
|
+
setOperatorLabel(typeof op?.displayName === 'string' ? op.displayName : (typeof op?.id === 'string' ? op.id : null));
|
|
141
150
|
},
|
|
142
151
|
})
|
|
143
152
|
.then((ctx) => {
|
|
144
153
|
const af = ctx?.royaltiSuite?.activeFeature;
|
|
145
154
|
if (typeof af === 'string') setActiveFeature(af);
|
|
155
|
+
const op = ctx?.operator;
|
|
156
|
+
setOperatorId(typeof op?.id === 'string' ? op.id : null);
|
|
157
|
+
setOperatorLabel(typeof op?.displayName === 'string' ? op.displayName : (typeof op?.id === 'string' ? op.id : null));
|
|
146
158
|
setBridgeReady(true);
|
|
147
159
|
})
|
|
148
160
|
.catch((e) => setBridgeError(e.message ?? String(e)));
|
|
@@ -155,7 +167,7 @@ function App() {
|
|
|
155
167
|
return html`<div style=${{ padding: '2rem', color: 'var(--fg-muted)' }}>Connecting…</div>`;
|
|
156
168
|
}
|
|
157
169
|
|
|
158
|
-
return html`<${QueryClientProvider} client=${queryClient}><${SalesView} activeFeature=${activeFeature} /></${QueryClientProvider}>`;
|
|
170
|
+
return html`<${QueryClientProvider} client=${queryClient}><${SalesView} activeFeature=${activeFeature} operatorId=${operatorId} operatorLabel=${operatorLabel} /></${QueryClientProvider}>`;
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
createRoot(document.getElementById('root')).render(html`<${App} />`);
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// - .kb-board / .kb-col / .kb-card / .kb-mini-avatar / .kb-add (part 28 kanban)
|
|
12
12
|
// - .nav-group[data-kind] / .nav-item / .nav-item.is-on / .nav-item.is-hot (part 22)
|
|
13
13
|
// - .seg.nav-view-seg / .seg button.is-on (part 14 segmented-tabs, list-kanban-switch)
|
|
14
|
-
// - .atelier-state.is-{loading,empty,error
|
|
14
|
+
// - .atelier-state.is-{loading,empty,error} / .atelier-spin (part 26 feedback-state)
|
|
15
15
|
// - .stage-chip / .next-chip / .ux-dot / .badge / .tag / .chip (part 11 badge-tag-chip)
|
|
16
16
|
// - .btn / .btn-icon / .btn-sm / .btn.affirmative (part 10 buttons)
|
|
17
17
|
//
|
|
@@ -31,6 +31,17 @@ import {
|
|
|
31
31
|
useQuery, useMutation, useQueryClient,
|
|
32
32
|
} from '../../lib/ui.js';
|
|
33
33
|
import { hostDbQuery, hostDbExec, setMenu, isStandalone } from '../../lib/bridge.js';
|
|
34
|
+
// create-wire recipe: dead "+ / New deal" buttons dispatch a creation brief to
|
|
35
|
+
// the Chi (R-03: a deal is agent-shaped, never a client-side husk INSERT).
|
|
36
|
+
import { buildCreateBrief, dispatchCreate } from '../../lib/create-dispatch.js';
|
|
37
|
+
// dispatch-wire recipe: deal-detail "Approve & run"/"Confirm & run" seed a
|
|
38
|
+
// structured next-action turn into the active Chi (host.sendToActiveSession).
|
|
39
|
+
import { dispatchItemAction } from '../../lib/dispatch.js';
|
|
40
|
+
// facet-wire recipe: sidebar filter facets (f:*) narrow the pipeline list/kanban.
|
|
41
|
+
import { applyFacet } from '../../lib/facet-filter.js';
|
|
42
|
+
// operator-identity recipe: hostContext.operator threaded down from app.js —
|
|
43
|
+
// "mine" predicates/fallbacks fail safe (empty/unclaimed) when unknown.
|
|
44
|
+
import { isMine } from '../../lib/operator.js';
|
|
34
45
|
|
|
35
46
|
// ─── Stage enum ───────────────────────────────────────────────────────────────
|
|
36
47
|
// Per the R-04 Pipeline-stages convention (06-skill-action-contract.md §Pipeline-stages).
|
|
@@ -131,7 +142,7 @@ async function fetchOpenDeals() {
|
|
|
131
142
|
}
|
|
132
143
|
}
|
|
133
144
|
|
|
134
|
-
async function fetchWonDeals() {
|
|
145
|
+
async function fetchWonDeals(operatorId) {
|
|
135
146
|
if (isStandalone()) return WON_DEALS_FIXTURE;
|
|
136
147
|
try {
|
|
137
148
|
const rows = await hostDbQuery(
|
|
@@ -145,7 +156,7 @@ async function fetchWonDeals() {
|
|
|
145
156
|
return rows.map((r) => ({
|
|
146
157
|
...r,
|
|
147
158
|
title: r.title ?? r.company,
|
|
148
|
-
owner: r.owner ?? r.assigned_to ??
|
|
159
|
+
owner: r.owner ?? r.assigned_to ?? operatorId ?? null,
|
|
149
160
|
closed: r.last_contact ? r.last_contact.substring(0, 10) : '—',
|
|
150
161
|
value: typeof r.value === 'string' ? parseFloat(r.value) || 0 : (r.value ?? 0),
|
|
151
162
|
}));
|
|
@@ -186,6 +197,51 @@ function fmtPct(v) {
|
|
|
186
197
|
return `${Math.round(v * 100)}%`;
|
|
187
198
|
}
|
|
188
199
|
|
|
200
|
+
// ─── dispatch-wire (RECIPE 1) ─────────────────────────────────────────────────
|
|
201
|
+
// Map a deal row into the recipe-shared descriptor, then seed a next-action turn
|
|
202
|
+
// into the active Chi. A deal is agent-shaped, so "Approve & run"/"Confirm & run"
|
|
203
|
+
// dispatch a structured turn (host.sendToActiveSession) rather than committing
|
|
204
|
+
// headlessly — there is no domain-run verb today (see lib/dispatch.js APPROVE-RUN GAP).
|
|
205
|
+
|
|
206
|
+
function dealToDispatchItem(deal) {
|
|
207
|
+
return {
|
|
208
|
+
kind: 'deal',
|
|
209
|
+
title: deal.title ?? deal.company,
|
|
210
|
+
stage: STAGE_LABEL[deal.stage] ?? deal.stage,
|
|
211
|
+
nextAction: deal.next_action,
|
|
212
|
+
facts: [
|
|
213
|
+
['Company', deal.company],
|
|
214
|
+
['Owner', deal.owner ?? deal.assigned_to],
|
|
215
|
+
['Value', deal.value != null ? fmtCurrency(deal.value) : null],
|
|
216
|
+
],
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function handleAction(deal) {
|
|
221
|
+
const mode = deal.next_action_mode === 'approve' ? 'approve' : 'confirm';
|
|
222
|
+
dispatchItemAction(dealToDispatchItem(deal), mode, 'com.ikenga.sales').catch(() => {});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ─── facet-wire (RECIPE 2) ────────────────────────────────────────────────────
|
|
226
|
+
// Sales' "all" affordance is 'f:open-pipeline' (not the generic 'f:all'), so the
|
|
227
|
+
// reset id is passed explicitly to applyFacet. Each predicate expression MIRRORS
|
|
228
|
+
// its badge-count expression in buildSalesMenu so a facet's slice always matches
|
|
229
|
+
// the count shown on its row (facet-wire pitfall 2).
|
|
230
|
+
|
|
231
|
+
const SALES_RESET_FACET = 'f:open-pipeline';
|
|
232
|
+
|
|
233
|
+
/** operatorId-parameterized so 'f:my-deals' fails safe (matches nothing) when
|
|
234
|
+
* the operator is unknown — see lib/operator.js. */
|
|
235
|
+
function salesFacetPredicates(operatorId) {
|
|
236
|
+
return {
|
|
237
|
+
'f:my-deals': (d) => isMine(d.owner, operatorId) || isMine(d.assigned_to, operatorId),
|
|
238
|
+
'f:closing-soon': (d) => d.next_action_mode === 'approve',
|
|
239
|
+
'f:agent-run': (d) => d.owner === 'sales-agent' || d.assigned_to === 'sales-agent',
|
|
240
|
+
...Object.fromEntries(STAGES.map((s) => [`f:stage:${s}`, (d) => d.stage === s])),
|
|
241
|
+
// 'f:open-pipeline' intentionally ABSENT → SALES_RESET_FACET returns every deal.
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
189
245
|
// ─── Menu builder ─────────────────────────────────────────────────────────────
|
|
190
246
|
|
|
191
247
|
/**
|
|
@@ -193,12 +249,15 @@ function fmtPct(v) {
|
|
|
193
249
|
* activeView: 0 = Pipeline | 1 = Forecast | 2 = Won
|
|
194
250
|
* pipeMode: 'list' | 'kanban' (only relevant when activeView === 0)
|
|
195
251
|
* deals: the open deals array (for counts)
|
|
252
|
+
* operatorId: current known operator id (null when unknown — see lib/operator.js)
|
|
196
253
|
*/
|
|
197
|
-
function buildSalesMenu(activeView, pipeMode, deals) {
|
|
254
|
+
function buildSalesMenu(activeView, pipeMode, deals, activeFacet, operatorId) {
|
|
198
255
|
const openCount = deals.length;
|
|
199
|
-
const myDeals = deals.filter((d) => d.owner
|
|
256
|
+
const myDeals = deals.filter((d) => isMine(d.owner, operatorId) || isMine(d.assigned_to, operatorId));
|
|
200
257
|
const closingSoon = deals.filter((d) => d.next_action_mode === 'approve');
|
|
201
258
|
const agentRun = deals.filter((d) => (d.owner === 'sales-agent' || d.assigned_to === 'sales-agent'));
|
|
259
|
+
// A facet only highlights on the Pipeline view (facets dim/inert off it).
|
|
260
|
+
const facetActive = (id) => activeView === 0 && activeFacet === id;
|
|
202
261
|
|
|
203
262
|
const viewItems = [
|
|
204
263
|
{
|
|
@@ -248,7 +307,7 @@ function buildSalesMenu(activeView, pipeMode, deals) {
|
|
|
248
307
|
label: 'Open pipeline',
|
|
249
308
|
icon: 'layers',
|
|
250
309
|
section: 'Filters',
|
|
251
|
-
active:
|
|
310
|
+
active: facetActive('f:open-pipeline'),
|
|
252
311
|
badge: openCount,
|
|
253
312
|
disabled: activeView !== 0,
|
|
254
313
|
},
|
|
@@ -257,7 +316,7 @@ function buildSalesMenu(activeView, pipeMode, deals) {
|
|
|
257
316
|
label: 'My deals',
|
|
258
317
|
icon: 'user',
|
|
259
318
|
section: 'Filters',
|
|
260
|
-
active:
|
|
319
|
+
active: facetActive('f:my-deals'),
|
|
261
320
|
badge: myDeals.length || undefined,
|
|
262
321
|
disabled: activeView !== 0,
|
|
263
322
|
},
|
|
@@ -266,7 +325,7 @@ function buildSalesMenu(activeView, pipeMode, deals) {
|
|
|
266
325
|
label: 'Closing soon',
|
|
267
326
|
icon: 'zap',
|
|
268
327
|
section: 'Filters',
|
|
269
|
-
active:
|
|
328
|
+
active: facetActive('f:closing-soon'),
|
|
270
329
|
hot: closingSoon.length > 0,
|
|
271
330
|
badge: closingSoon.length > 0 ? closingSoon.length : undefined,
|
|
272
331
|
disabled: activeView !== 0,
|
|
@@ -276,7 +335,7 @@ function buildSalesMenu(activeView, pipeMode, deals) {
|
|
|
276
335
|
label: 'Agent-run',
|
|
277
336
|
icon: 'cpu',
|
|
278
337
|
section: 'Filters',
|
|
279
|
-
active:
|
|
338
|
+
active: facetActive('f:agent-run'),
|
|
280
339
|
badge: agentRun.length || undefined,
|
|
281
340
|
disabled: activeView !== 0,
|
|
282
341
|
},
|
|
@@ -286,7 +345,7 @@ function buildSalesMenu(activeView, pipeMode, deals) {
|
|
|
286
345
|
id: `f:stage:${s}`,
|
|
287
346
|
label: STAGE_LABEL[s],
|
|
288
347
|
section: 'By stage',
|
|
289
|
-
active:
|
|
348
|
+
active: facetActive(`f:stage:${s}`),
|
|
290
349
|
badge: deals.filter((d) => d.stage === s).length || undefined,
|
|
291
350
|
disabled: activeView !== 0,
|
|
292
351
|
}));
|
|
@@ -335,6 +394,12 @@ function DealRow({ deal, isSelected, onClick }) {
|
|
|
335
394
|
|
|
336
395
|
/** Deal detail pane */
|
|
337
396
|
function DealDetail({ deal, activities }) {
|
|
397
|
+
// dispatch-wire — local feedback: flips true on click so the operator sees the
|
|
398
|
+
// hand-off landed and the same click can't double-seed the session. Resets when
|
|
399
|
+
// the selected deal changes.
|
|
400
|
+
const [sent, setSent] = useState(false);
|
|
401
|
+
useEffect(() => { setSent(false); }, [deal?.id]);
|
|
402
|
+
|
|
338
403
|
if (!deal) {
|
|
339
404
|
return html`<div class="ip-split-pane split-detail" style=${{ display:'flex', alignItems:'center', justifyContent:'center', color:'var(--fg-muted)', fontSize:'0.85rem' }}>
|
|
340
405
|
Select a deal
|
|
@@ -344,12 +409,19 @@ function DealDetail({ deal, activities }) {
|
|
|
344
409
|
const hasApprove = deal.next_action_mode === 'approve';
|
|
345
410
|
const hasConfirm = deal.next_action_mode === 'confirm';
|
|
346
411
|
const showButton = hasApprove || hasConfirm;
|
|
412
|
+
const onAct = () => { handleAction(deal); setSent(true); };
|
|
347
413
|
|
|
348
414
|
return html`
|
|
349
415
|
<div class="ip-split-pane split-detail" style=${{ overflowY:'auto' }}>
|
|
350
416
|
<div class="split-detail-wrap">
|
|
351
417
|
<div class="split-detail-eyebrow">
|
|
352
418
|
<span class="stage-chip">${STAGE_LABEL[deal.stage] ?? deal.stage}</span>
|
|
419
|
+
${deal.next_action_mode ? html`
|
|
420
|
+
<span class="next-chip">
|
|
421
|
+
<span class=${cn('ux-dot', `ux-${deal.next_action_mode}`)}></span>
|
|
422
|
+
ux_mode · ${deal.next_action_mode}
|
|
423
|
+
</span>
|
|
424
|
+
` : null}
|
|
353
425
|
</div>
|
|
354
426
|
<div class="split-detail-title">${deal.title ?? deal.company}</div>
|
|
355
427
|
<div class="split-detail-sub">${deal.company}${deal.owner ? ` · ${deal.owner}` : ''}</div>
|
|
@@ -371,8 +443,13 @@ function DealDetail({ deal, activities }) {
|
|
|
371
443
|
${deal.next_action}
|
|
372
444
|
${showButton ? html`
|
|
373
445
|
<div style=${{ marginTop: '10px' }}>
|
|
374
|
-
<button
|
|
375
|
-
|
|
446
|
+
<button
|
|
447
|
+
class=${cn('btn', hasApprove ? 'affirmative' : '')}
|
|
448
|
+
type="button"
|
|
449
|
+
disabled=${sent}
|
|
450
|
+
onClick=${onAct}
|
|
451
|
+
>
|
|
452
|
+
${sent ? 'Sent to your Chi' : hasApprove ? 'Approve & run' : 'Confirm & run'}
|
|
376
453
|
</button>
|
|
377
454
|
</div>
|
|
378
455
|
` : null}
|
|
@@ -381,7 +458,7 @@ function DealDetail({ deal, activities }) {
|
|
|
381
458
|
` : null}
|
|
382
459
|
|
|
383
460
|
${activities && activities.length > 0 ? html`
|
|
384
|
-
<div class="split-
|
|
461
|
+
<div class="split-next-head" style=${{ marginTop:'12px' }}>Activity</div>
|
|
385
462
|
<div class="split-timeline" role="list">
|
|
386
463
|
${activities.map((a) => html`
|
|
387
464
|
<div class="split-tl-row" role="listitem" key=${a.id}>
|
|
@@ -446,7 +523,7 @@ function KbAvatar({ owner }) {
|
|
|
446
523
|
}
|
|
447
524
|
|
|
448
525
|
/** Pipeline kanban mode */
|
|
449
|
-
function PipelineKanban({ deals, onStageChange }) {
|
|
526
|
+
function PipelineKanban({ deals, onStageChange, onCreate }) {
|
|
450
527
|
const [dragging, setDragging] = useState(null);
|
|
451
528
|
const [dropTarget, setDropTarget] = useState(null);
|
|
452
529
|
|
|
@@ -512,7 +589,12 @@ function PipelineKanban({ deals, onStageChange }) {
|
|
|
512
589
|
</div>
|
|
513
590
|
</div>
|
|
514
591
|
`)}
|
|
515
|
-
<button
|
|
592
|
+
<button
|
|
593
|
+
class="kb-add btn-icon"
|
|
594
|
+
type="button"
|
|
595
|
+
aria-label=${'Add deal to ' + (STAGE_LABEL[s] ?? s)}
|
|
596
|
+
onClick=${() => onCreate?.(s)}
|
|
597
|
+
>+</button>
|
|
516
598
|
</div>
|
|
517
599
|
</div>
|
|
518
600
|
`;
|
|
@@ -598,7 +680,7 @@ function ForecastView({ deals }) {
|
|
|
598
680
|
<div class="sl-month" key=${m.label}>
|
|
599
681
|
<span class="sl-month-val">${fmtCurrency(m.val)}</span>
|
|
600
682
|
<div class="sl-month-bar-wrap">
|
|
601
|
-
<div class="sl-month-bar" style=${{ height: `${Math.round((m.val / maxMonthVal) *
|
|
683
|
+
<div class="sl-month-bar" style=${{ height: `${Math.round((m.val / maxMonthVal) * 100)}%` }}></div>
|
|
602
684
|
</div>
|
|
603
685
|
<span class="sl-month-lab">${m.label}</span>
|
|
604
686
|
</div>
|
|
@@ -680,35 +762,40 @@ function LoadingState() {
|
|
|
680
762
|
`;
|
|
681
763
|
}
|
|
682
764
|
|
|
683
|
-
function EmptyState() {
|
|
765
|
+
function EmptyState({ onCreate }) {
|
|
684
766
|
return html`
|
|
685
767
|
<div class="atelier-state is-empty" id="view-stage">
|
|
686
768
|
<span>No deals yet</span>
|
|
687
|
-
<button
|
|
769
|
+
<button
|
|
770
|
+
class="btn btn-sm"
|
|
771
|
+
type="button"
|
|
772
|
+
style=${{ marginTop:'8px' }}
|
|
773
|
+
onClick=${() => onCreate?.()}
|
|
774
|
+
>New deal</button>
|
|
688
775
|
</div>
|
|
689
776
|
`;
|
|
690
777
|
}
|
|
691
778
|
|
|
692
|
-
|
|
779
|
+
// Error state — design STATES.error (atelier-sales-list.html:1877-1879): alert
|
|
780
|
+
// icon + heading + explanation + a Retry button that refetches.
|
|
781
|
+
function ErrorState({ error, onRetry }) {
|
|
693
782
|
return html`
|
|
694
783
|
<div class="atelier-state is-error" id="view-stage">
|
|
695
|
-
<
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
<div class="atelier-state is-streaming" id="view-stage">
|
|
704
|
-
<span class="atelier-prog" role="status" aria-live="polite">Sales agent running…</span>
|
|
784
|
+
<svg class="ix" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
785
|
+
<path d="M12 9v4"/><path d="M12 17h.01"/>
|
|
786
|
+
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/>
|
|
787
|
+
</svg>
|
|
788
|
+
<h3>Couldn’t load sales</h3>
|
|
789
|
+
<p>The source didn’t respond. Retry, or ask your Chi to check the connection.</p>
|
|
790
|
+
${onRetry ? html`<button class="btn btn-sm" type="button" onClick=${onRetry}>Retry</button>` : null}
|
|
791
|
+
${error ? html`<span style=${{ fontSize:'0.72rem', color:'var(--fg-muted)', marginTop:'4px' }}>${error}</span>` : null}
|
|
705
792
|
</div>
|
|
706
793
|
`;
|
|
707
794
|
}
|
|
708
795
|
|
|
709
796
|
// ─── SalesView root ───────────────────────────────────────────────────────────
|
|
710
797
|
|
|
711
|
-
export function SalesView({ activeFeature }) {
|
|
798
|
+
export function SalesView({ activeFeature, operatorId }) {
|
|
712
799
|
// View state: 0=Pipeline | 1=Forecast | 2=Won
|
|
713
800
|
const [activeView, setActiveView] = useState(() => {
|
|
714
801
|
const p = new URLSearchParams(window.location.search).get('view');
|
|
@@ -717,11 +804,18 @@ export function SalesView({ activeFeature }) {
|
|
|
717
804
|
});
|
|
718
805
|
const [pipeMode, setPipeMode] = useState('list'); // 'list' | 'kanban'
|
|
719
806
|
const [selectedDeal, setSelectedDeal] = useState(null);
|
|
807
|
+
// facet-wire — last-applied sidebar filter facet. 'f:open-pipeline' is the
|
|
808
|
+
// reset/"all" affordance (no predicate → applyFacet returns every deal).
|
|
809
|
+
const [activeFacet, setActiveFacet] = useState(SALES_RESET_FACET);
|
|
720
810
|
const qc = useQueryClient();
|
|
721
811
|
|
|
722
812
|
// ── Data queries ────────────────────────────────────────────────────────────
|
|
723
813
|
const openDealsQ = useQuery({ queryKey: QK.openDeals, queryFn: fetchOpenDeals });
|
|
724
|
-
const wonDealsQ = useQuery({
|
|
814
|
+
const wonDealsQ = useQuery({
|
|
815
|
+
queryKey: QK.wonDeals,
|
|
816
|
+
queryFn: () => fetchWonDeals(operatorId),
|
|
817
|
+
enabled: activeView === 2,
|
|
818
|
+
});
|
|
725
819
|
|
|
726
820
|
const activitiesQ = useQuery({
|
|
727
821
|
queryKey: QK.activities(selectedDeal?.id ?? null),
|
|
@@ -731,6 +825,14 @@ export function SalesView({ activeFeature }) {
|
|
|
731
825
|
});
|
|
732
826
|
|
|
733
827
|
const deals = openDealsQ.data ?? [];
|
|
828
|
+
// facet-wire — the visible slice: full list narrowed by the active facet.
|
|
829
|
+
// Feeds the list, kanban AND the empty-state check (so a facet that matches
|
|
830
|
+
// nothing shows the empty pane, not a stale full list). Badges stay live
|
|
831
|
+
// because buildSalesMenu counts the FULL `deals`, not this slice.
|
|
832
|
+
const visibleDeals = useMemo(
|
|
833
|
+
() => applyFacet(deals, activeFacet, salesFacetPredicates(operatorId), SALES_RESET_FACET),
|
|
834
|
+
[deals, activeFacet, operatorId],
|
|
835
|
+
);
|
|
734
836
|
|
|
735
837
|
// ── db-updated refresh ──────────────────────────────────────────────────────
|
|
736
838
|
useEffect(() => {
|
|
@@ -749,6 +851,13 @@ export function SalesView({ activeFeature }) {
|
|
|
749
851
|
else if (activeFeature === 'v:won') setActiveView(2);
|
|
750
852
|
else if (activeFeature === 'seg:list') setPipeMode('list');
|
|
751
853
|
else if (activeFeature === 'seg:kanban') setPipeMode('kanban');
|
|
854
|
+
// Filter facets (f:*) — LIST-ONLY: force the Pipeline view so a facet the
|
|
855
|
+
// user can't see can't be silently applied, then record it. 'f:open-pipeline'
|
|
856
|
+
// resets; every other id narrows via applyFacet. (facet-wire Move 2.)
|
|
857
|
+
else if (activeFeature.startsWith('f:')) {
|
|
858
|
+
setActiveView(0);
|
|
859
|
+
setActiveFacet(activeFeature);
|
|
860
|
+
}
|
|
752
861
|
}, [activeFeature]);
|
|
753
862
|
|
|
754
863
|
// ── Pre-select hero deal (D-05 — Catalog onboarding · Chocolate City) ──────
|
|
@@ -762,9 +871,9 @@ export function SalesView({ activeFeature }) {
|
|
|
762
871
|
// ── setMenu publish ─────────────────────────────────────────────────────────
|
|
763
872
|
useEffect(() => {
|
|
764
873
|
if (isStandalone()) return;
|
|
765
|
-
const items = buildSalesMenu(activeView, pipeMode, deals);
|
|
874
|
+
const items = buildSalesMenu(activeView, pipeMode, deals, activeFacet, operatorId);
|
|
766
875
|
setMenu(items).catch(() => {/* ignore */});
|
|
767
|
-
}, [activeView, pipeMode, deals]);
|
|
876
|
+
}, [activeView, pipeMode, deals, activeFacet, operatorId]);
|
|
768
877
|
|
|
769
878
|
// ── Stage change mutation (kanban drag) ─────────────────────────────────────
|
|
770
879
|
const stageChange = useMutation({
|
|
@@ -778,6 +887,29 @@ export function SalesView({ activeFeature }) {
|
|
|
778
887
|
onSuccess: () => qc.invalidateQueries({ queryKey: QK.openDeals }),
|
|
779
888
|
});
|
|
780
889
|
|
|
890
|
+
// ── Dispatch-mode creation (create-wire recipe) ─────────────────────────────
|
|
891
|
+
// R-03: a deal is agent-shaped — it needs company research plus owner,
|
|
892
|
+
// next_action, next_action_mode and win_probability, and links to `contacts`.
|
|
893
|
+
// Seeding an empty client-side INSERT would leave a husk the user must
|
|
894
|
+
// hand-fill, so creation dispatches a structured brief to the active Chi
|
|
895
|
+
// session instead (host.sendToActiveSession, via lib/create-dispatch.js).
|
|
896
|
+
// `stage` is the kanban column's pre-filled context; undefined for the
|
|
897
|
+
// empty-state "New deal" which seeds a full brief.
|
|
898
|
+
const createDeal = useCallback((stage) => {
|
|
899
|
+
const label = stage ? (STAGE_LABEL[stage] ?? stage) : null;
|
|
900
|
+
const brief = buildCreateBrief({
|
|
901
|
+
entity: 'sales deal',
|
|
902
|
+
table: 'sales_deals',
|
|
903
|
+
seed: label ? { stage: label } : {},
|
|
904
|
+
instruction:
|
|
905
|
+
'Research the company, then set the title, company, owner, value, '
|
|
906
|
+
+ 'next action, and win probability'
|
|
907
|
+
+ (label ? `, and file it at the ${label} stage` : '')
|
|
908
|
+
+ '. Ask me for anything you still need, then add it to the sales_deals table.',
|
|
909
|
+
});
|
|
910
|
+
void dispatchCreate(brief, 'com.ikenga.sales');
|
|
911
|
+
}, []);
|
|
912
|
+
|
|
781
913
|
// ── Head label ──────────────────────────────────────────────────────────────
|
|
782
914
|
const headLabel = activeView === 0 ? `Sales · ${deals.length} open`
|
|
783
915
|
: activeView === 1 ? 'Forecast'
|
|
@@ -791,17 +923,18 @@ export function SalesView({ activeFeature }) {
|
|
|
791
923
|
if (openDealsQ.isLoading) {
|
|
792
924
|
body = html`<${LoadingState} />`;
|
|
793
925
|
} else if (openDealsQ.isError) {
|
|
794
|
-
body = html`<${ErrorState} error=${openDealsQ.error?.message ?? 'unknown'} />`;
|
|
795
|
-
} else if (
|
|
796
|
-
body = html`<${EmptyState} />`;
|
|
926
|
+
body = html`<${ErrorState} error=${openDealsQ.error?.message ?? 'unknown'} onRetry=${() => openDealsQ.refetch()} />`;
|
|
927
|
+
} else if (visibleDeals.length === 0) {
|
|
928
|
+
body = html`<${EmptyState} onCreate=${createDeal} />`;
|
|
797
929
|
} else if (pipeMode === 'kanban') {
|
|
798
930
|
body = html`<${PipelineKanban}
|
|
799
|
-
deals=${
|
|
931
|
+
deals=${visibleDeals}
|
|
800
932
|
onStageChange=${(deal, newStage) => stageChange.mutate({ deal, newStage })}
|
|
933
|
+
onCreate=${createDeal}
|
|
801
934
|
/>`;
|
|
802
935
|
} else {
|
|
803
936
|
body = html`<${PipelineList}
|
|
804
|
-
deals=${
|
|
937
|
+
deals=${visibleDeals}
|
|
805
938
|
selectedDeal=${selectedDeal}
|
|
806
939
|
onSelectDeal=${setSelectedDeal}
|
|
807
940
|
activities=${activitiesQ.data ?? []}
|
|
@@ -814,7 +947,7 @@ export function SalesView({ activeFeature }) {
|
|
|
814
947
|
if (wonDealsQ.isLoading) {
|
|
815
948
|
body = html`<${LoadingState} />`;
|
|
816
949
|
} else if (wonDealsQ.isError) {
|
|
817
|
-
body = html`<${ErrorState} error=${wonDealsQ.error?.message ?? 'unknown'} />`;
|
|
950
|
+
body = html`<${ErrorState} error=${wonDealsQ.error?.message ?? 'unknown'} onRetry=${() => wonDealsQ.refetch()} />`;
|
|
818
951
|
} else {
|
|
819
952
|
body = html`<${WonView} wonDeals=${wonDealsQ.data ?? WON_DEALS_FIXTURE} />`;
|
|
820
953
|
}
|
package/dist/lib/bridge.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
// MCP Apps SDK bridge — the canonical iframe⇄host protocol Ikenga uses.
|
|
2
2
|
//
|
|
3
|
+
// SOURCE OF TRUTH (WP-19). This file is the single, byte-identical bridge core
|
|
4
|
+
// vendored into every no-build app pkg's dist/lib/bridge.js. The two per-pkg
|
|
5
|
+
// identifiers that used to be hand-edited into each copy — the source-id passed
|
|
6
|
+
// to host.sendToActiveSession and the short log tag in error/console strings —
|
|
7
|
+
// are now injected via the generated sibling `./pkg-id.js` (PKG_ID, LOG_TAG),
|
|
8
|
+
// which kills the copy-paste-id bug class (a `tasks`/`sales` id or `[sales]`
|
|
9
|
+
// tag left behind in the wrong pkg). Never hand-edit a vendored copy; edit here
|
|
10
|
+
// and re-run the pkg's `scripts/build.mjs`.
|
|
11
|
+
//
|
|
3
12
|
// Pattern from @modelcontextprotocol/ext-apps Quickstart + the shell's
|
|
4
13
|
// pkg-iframe-host.tsx implementation:
|
|
5
14
|
// 1. new App(...) — register handlers before connect
|
|
@@ -21,6 +30,7 @@
|
|
|
21
30
|
// clobbers our workspace `data-theme` (A/B/C) with 'light'|'dark', breaking the
|
|
22
31
|
// bundled @ikenga/tokens palette — so the bridge stays out of theming entirely.
|
|
23
32
|
import { App } from 'https://esm.sh/@modelcontextprotocol/ext-apps@1.7.1/app-with-deps';
|
|
33
|
+
import { PKG_ID, LOG_TAG } from './pkg-id.js';
|
|
24
34
|
|
|
25
35
|
let app = null;
|
|
26
36
|
|
|
@@ -31,7 +41,7 @@ export async function connectBridge({ name, version, onContextChange }) {
|
|
|
31
41
|
tools: { listChanged: false },
|
|
32
42
|
});
|
|
33
43
|
|
|
34
|
-
app.onerror = (err) => console.error(
|
|
44
|
+
app.onerror = (err) => console.error(`[${LOG_TAG}] bridge error`, err);
|
|
35
45
|
// Theme is NOT applied here — app.js mirrors it from the parent <html>.
|
|
36
46
|
// We still forward context so live activeFeature (side-menu) updates reach
|
|
37
47
|
// the app; data flows through host.dbQuery/dbExec, not the context payload.
|
|
@@ -83,7 +93,7 @@ export async function setMenu(items) {
|
|
|
83
93
|
* prompt: string — the instruction shown as the user turn
|
|
84
94
|
* source?: string — provenance tag (defaults to the pkg id)
|
|
85
95
|
*/
|
|
86
|
-
export async function hostSendToActiveSession(prompt, source =
|
|
96
|
+
export async function hostSendToActiveSession(prompt, source = PKG_ID) {
|
|
87
97
|
if (!app) throw new Error('bridge not connected');
|
|
88
98
|
return app.callServerTool({
|
|
89
99
|
name: 'host.sendToActiveSession',
|
|
@@ -118,7 +128,7 @@ export async function hostPaActionsRun(args) {
|
|
|
118
128
|
* params: SqlValue[] — positional bind values
|
|
119
129
|
*/
|
|
120
130
|
export async function hostDbQuery(sql, params = []) {
|
|
121
|
-
if (!app) throw new Error(
|
|
131
|
+
if (!app) throw new Error(`[${LOG_TAG}] bridge not connected — db_query unavailable`);
|
|
122
132
|
const res = await app.callServerTool({
|
|
123
133
|
name: 'host.dbQuery',
|
|
124
134
|
arguments: { sql, params },
|
|
@@ -141,7 +151,7 @@ export async function hostDbQuery(sql, params = []) {
|
|
|
141
151
|
* params: SqlValue[] — positional bind values
|
|
142
152
|
*/
|
|
143
153
|
export async function hostDbExec(sql, params = []) {
|
|
144
|
-
if (!app) throw new Error(
|
|
154
|
+
if (!app) throw new Error(`[${LOG_TAG}] bridge not connected — db_exec unavailable`);
|
|
145
155
|
const res = await app.callServerTool({
|
|
146
156
|
name: 'host.dbExec',
|
|
147
157
|
arguments: { sql, params },
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// RECIPE-SHARED SHAPE (create-wire) — dispatch-mode creation for domain pkgs.
|
|
2
|
+
//
|
|
3
|
+
// Lift target: @ikenga/pkg-runtime. Every domain pkg's dead "+ / New <X>"
|
|
4
|
+
// buttons route through here so creation is *agent-shaped* — we seed a
|
|
5
|
+
// structured creation brief into the shell's active Chi session and let the
|
|
6
|
+
// agent do the create through its skill (research, cross-table links, enrich
|
|
7
|
+
// fields), instead of the pkg writing a raw client-side husk INSERT.
|
|
8
|
+
//
|
|
9
|
+
// This is the R4 / R-03 rule made concrete: domain pkgs never run transport or
|
|
10
|
+
// CRUD-for-agents directly. Creation of a row that needs enrichment or
|
|
11
|
+
// cross-table fan-out is dispatch. A narrow direct-write exception exists —
|
|
12
|
+
// see create-wire.md "Decision rule" — for a self-contained, fully
|
|
13
|
+
// user-supplied single-table row (the tasks CreateTaskForm case).
|
|
14
|
+
//
|
|
15
|
+
// SEAM (must exist — do not invent):
|
|
16
|
+
// host.sendToActiveSession({ prompt, source? })
|
|
17
|
+
// → shell handler: shell/src/components/pkg/pkg-iframe-host.tsx:551
|
|
18
|
+
// → bridge wrapper: lib/bridge.js (hostSendToActiveSession)
|
|
19
|
+
// Returns { ok, threadId?, reason? }. Refuses with reason:'no-active-session'
|
|
20
|
+
// when no chat pane is focused.
|
|
21
|
+
//
|
|
22
|
+
// PRECONDITION (currently a shell/contract gap — see create-wire.md pitfall #1):
|
|
23
|
+
// The verb is gated on the `engine:invoke` scope (pkg-iframe-host.tsx:562),
|
|
24
|
+
// which the FE resolves from manifest `permissions.engine` containing
|
|
25
|
+
// 'invoke'. Neither the contract PermissionsSchema nor the Rust Permissions
|
|
26
|
+
// struct carries an `engine` key today, so it is stripped and the gate
|
|
27
|
+
// returns scope-denied. Wire it correctly anyway (this file); it lights up
|
|
28
|
+
// the moment the shell adds the key. dispatchCreate swallows the refusal so
|
|
29
|
+
// a blocked gate never throws into the view.
|
|
30
|
+
|
|
31
|
+
import { hostSendToActiveSession, isStandalone } from './bridge.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a structured creation brief for the Chi.
|
|
35
|
+
*
|
|
36
|
+
* The SHAPE is constant across domains so every "New <X>" reads the same to the
|
|
37
|
+
* agent: a one-line "Create a new <entity>." heading, an optional fielded
|
|
38
|
+
* "Known so far:" block seeded from the click context, and an explicit
|
|
39
|
+
* instruction that names the target table so the agent files it correctly.
|
|
40
|
+
* Domains vary only the entity label, table name, seed fields, and instruction.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} args
|
|
43
|
+
* @param {string} args.entity Human label, e.g. 'sales deal'.
|
|
44
|
+
* @param {string} args.table Target table, e.g. 'sales_deals'.
|
|
45
|
+
* @param {Record<string, string|number|null|undefined>} [args.seed]
|
|
46
|
+
* Pre-filled context from the click (e.g. { stage: 'Proposal' }).
|
|
47
|
+
* Null/empty values are dropped.
|
|
48
|
+
* @param {string} [args.instruction] What the agent should do before writing.
|
|
49
|
+
* Defaults to a generic "ask then insert into <table>".
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function buildCreateBrief({ entity, table, seed = {}, instruction }) {
|
|
53
|
+
const lines = [`Create a new ${entity}.`];
|
|
54
|
+
const entries = Object.entries(seed).filter(
|
|
55
|
+
([, v]) => v !== null && v !== undefined && v !== '',
|
|
56
|
+
);
|
|
57
|
+
if (entries.length) {
|
|
58
|
+
lines.push('', 'Known so far:');
|
|
59
|
+
for (const [k, v] of entries) lines.push(`- ${k}: ${v}`);
|
|
60
|
+
}
|
|
61
|
+
lines.push(
|
|
62
|
+
'',
|
|
63
|
+
instruction ??
|
|
64
|
+
`Ask me for anything you still need, then add it to the ${table} table.`,
|
|
65
|
+
);
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Seed a creation brief into the shell's active Chi session.
|
|
71
|
+
*
|
|
72
|
+
* Fire-and-forget by contract: no-ops (returns false) in standalone preview
|
|
73
|
+
* where there is no host, and never rethrows — a scope-denied / no-active-
|
|
74
|
+
* session refusal is logged, not surfaced as an exception, so a dead gate can
|
|
75
|
+
* never break the calling view. Returns true only when the dispatch call
|
|
76
|
+
* resolved.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} brief Output of buildCreateBrief.
|
|
79
|
+
* @param {string} source Provenance tag — pass the pkg id (e.g.
|
|
80
|
+
* 'com.ikenga.sales') so the shell stamps the audit trail
|
|
81
|
+
* with the right origin.
|
|
82
|
+
* @returns {Promise<boolean>}
|
|
83
|
+
*/
|
|
84
|
+
export async function dispatchCreate(brief, source) {
|
|
85
|
+
if (isStandalone()) return false;
|
|
86
|
+
try {
|
|
87
|
+
await hostSendToActiveSession(brief, source);
|
|
88
|
+
return true;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
console.warn('[create-dispatch] dispatch failed', e);
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Recipe-shared shape — atelier-parity RECIPE 1 · dispatch-wire.
|
|
2
|
+
//
|
|
3
|
+
// Lift-ready for a future @ikenga/pkg-runtime extraction: this module is the
|
|
4
|
+
// ONE place a domain pkg (content / research / strategy / sales) turns an
|
|
5
|
+
// operator "Approve & run" / "Confirm & run" click into a real host dispatch.
|
|
6
|
+
// It depends ONLY on hostSendToActiveSession + isStandalone from ./bridge.js
|
|
7
|
+
// (both present in every domain pkg's bridge copy), so it copies verbatim.
|
|
8
|
+
//
|
|
9
|
+
// SEAM RATIONALE (why sendToActiveSession, not a direct run):
|
|
10
|
+
// The shell's iframe host dispatcher (shell/src/components/pkg/
|
|
11
|
+
// pkg-iframe-host.tsx `dispatchHostCall`) supports exactly these host verbs:
|
|
12
|
+
// host.sendToActiveSession (line 551) ← the ONLY agent-run seam
|
|
13
|
+
// host.navigate, host.pkg.setMenu, host.dbQuery, host.dbExec,
|
|
14
|
+
// host.openSessionDialog, host.agentOps.*, host.fetch, host.invoke
|
|
15
|
+
// There is NO direct-run verb. `host.paActionsRun` and `host.paActions.*`
|
|
16
|
+
// both fall through to `return errResult('unknown host tool: ${name}')`
|
|
17
|
+
// (pkg-iframe-host.tsx:830) — do NOT wire to them. So BOTH ux_modes seed a
|
|
18
|
+
// structured user turn into the active Claude session; the agent performs
|
|
19
|
+
// the run through its own privileged path (same pattern the Tasks pkg uses to
|
|
20
|
+
// "create" work — see bridge.js hostSendToActiveSession docstring).
|
|
21
|
+
//
|
|
22
|
+
// APPROVE-RUN GAP (feeds WP-18 / the contract work):
|
|
23
|
+
// 'approve' cannot honestly run-without-a-turn today — there is no host verb
|
|
24
|
+
// that commits an action headlessly for a domain row. What 'approve' CAN do
|
|
25
|
+
// is frame the seeded turn as "operator has authority, run it now" so the
|
|
26
|
+
// agent proceeds without a back-and-forth. A true headless approve-run needs
|
|
27
|
+
// a new shell verb (e.g. host.paActions.commit actually wired in
|
|
28
|
+
// dispatchHostCall, or a domain-run verb). Until then approve == a
|
|
29
|
+
// differently-framed sendToActiveSession.
|
|
30
|
+
|
|
31
|
+
import { hostSendToActiveSession, isStandalone } from './bridge.js';
|
|
32
|
+
|
|
33
|
+
// Frame the seeded turn per ux_mode.
|
|
34
|
+
// approve → operator has authority, wants it done now (agent proceeds).
|
|
35
|
+
// confirm → operator wants the agent to confirm/plan before running.
|
|
36
|
+
const MODE_FRAME = {
|
|
37
|
+
approve: (label) => `Approve and run the next action for ${label}.`,
|
|
38
|
+
confirm: (label) => `Confirm the next action for ${label} with me before running it.`,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Build the structured user-turn prompt for an item's next action.
|
|
43
|
+
*
|
|
44
|
+
* `item` is the recipe-shared descriptor — each domain pkg maps its own row
|
|
45
|
+
* into this shape (see pieceToDispatchItem in content-view.js for the pattern):
|
|
46
|
+
* {
|
|
47
|
+
* kind: string human label for the entity ('content piece', 'deal', ...)
|
|
48
|
+
* title: string the entity title
|
|
49
|
+
* stage?: string current pipeline stage (already display-labelled)
|
|
50
|
+
* nextAction?:string the next_action text shown in the UI
|
|
51
|
+
* facts?: [k, v][] extra context lines ([['Channel','x'], ...]); nullish v skipped
|
|
52
|
+
* }
|
|
53
|
+
*
|
|
54
|
+
* Pure + side-effect-free so it is unit-testable without a bridge.
|
|
55
|
+
*/
|
|
56
|
+
export function buildActionPrompt(item, mode) {
|
|
57
|
+
const kind = item.kind ?? 'item';
|
|
58
|
+
const label = item.title ? `the ${kind} "${item.title}"` : `this ${kind}`;
|
|
59
|
+
const frame = (MODE_FRAME[mode] ?? MODE_FRAME.confirm)(label);
|
|
60
|
+
const lines = [frame, ''];
|
|
61
|
+
if (item.stage) lines.push(`Stage: ${item.stage}`);
|
|
62
|
+
if (item.nextAction) lines.push(`Next action: ${item.nextAction}`);
|
|
63
|
+
for (const [k, v] of item.facts ?? []) {
|
|
64
|
+
if (v != null && v !== '') lines.push(`${k}: ${v}`);
|
|
65
|
+
}
|
|
66
|
+
return lines.join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Dispatch an item's next action to the shell's active Claude session.
|
|
71
|
+
*
|
|
72
|
+
* Fire-and-forget seam: seeds a user turn via host.sendToActiveSession. Callers
|
|
73
|
+
* should still `.catch(() => {})` at the click site so a closed/failed bridge
|
|
74
|
+
* never throws into the handler (mirrors outbound-view.js `sendToChat`).
|
|
75
|
+
* No-ops in standalone (no parent shell). Returns the prompt that was sent (or
|
|
76
|
+
* null when standalone) so callers/tests can assert on it.
|
|
77
|
+
*
|
|
78
|
+
* item: descriptor (see buildActionPrompt)
|
|
79
|
+
* mode: 'approve' | 'confirm' (anything else → confirm framing)
|
|
80
|
+
* source: string provenance tag — pass the pkg id (e.g. 'com.ikenga.content')
|
|
81
|
+
*/
|
|
82
|
+
export async function dispatchItemAction(item, mode, source) {
|
|
83
|
+
if (isStandalone()) return null;
|
|
84
|
+
const prompt = buildActionPrompt(item, mode);
|
|
85
|
+
await hostSendToActiveSession(prompt, source);
|
|
86
|
+
return prompt;
|
|
87
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// facet-filter — recipe-shared shape (facet-wire recipe), lift-ready for a
|
|
2
|
+
// future @ikenga/pkg-runtime extraction. DO NOT add domain columns here; this
|
|
3
|
+
// stays domain-agnostic.
|
|
4
|
+
//
|
|
5
|
+
// Wire it fits: the shell relays a sidebar filter-facet click as
|
|
6
|
+
// `royaltiSuite.activeFeature = <the clicked PkgMenuItem id>` (e.g. 'f:mine')
|
|
7
|
+
// via the host-context re-emit — NEVER a pkg-menu-click message. The pkg keeps
|
|
8
|
+
// the last-applied facet id in state and calls `applyFacet` to derive the
|
|
9
|
+
// visible slice of its list. Pair this generic applier with a *domain* predicate
|
|
10
|
+
// map (facetId → (item) => boolean) declared in the view, where the domain
|
|
11
|
+
// columns live. Canonical consumer to mirror: tasks-view.js (FILTER_ITEMS + the
|
|
12
|
+
// activeFeature effect that narrows the list).
|
|
13
|
+
|
|
14
|
+
/** Reset/clear facet id — the "show everything" affordance. Tasks uses 'f:all'. */
|
|
15
|
+
export const RESET_FACET = 'f:all';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Return the slice of `items` that matches `facetId`.
|
|
19
|
+
*
|
|
20
|
+
* Inertness contract (matches tasks' behavior): a null/empty facet, the reset
|
|
21
|
+
* id, or an id with no predicate all return every item — an *unknown* facet is
|
|
22
|
+
* inert, never empty. So a stray dispatch can't blank the list.
|
|
23
|
+
*
|
|
24
|
+
* @template T
|
|
25
|
+
* @param {T[]} items full list
|
|
26
|
+
* @param {string | null | undefined} facetId last-applied facet id
|
|
27
|
+
* @param {Record<string, (item: T) => boolean>} predicates domain predicate map
|
|
28
|
+
* @param {string} [resetId] clear id (default 'f:all')
|
|
29
|
+
* @returns {T[]}
|
|
30
|
+
*/
|
|
31
|
+
export function applyFacet(items, facetId, predicates, resetId = RESET_FACET) {
|
|
32
|
+
if (!Array.isArray(items)) return [];
|
|
33
|
+
if (!facetId || facetId === resetId) return items;
|
|
34
|
+
const pred = predicates?.[facetId];
|
|
35
|
+
return typeof pred === 'function' ? items.filter(pred) : items;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** True for sidebar filter-facet ids (the 'f:' namespace, incl. 'f:t:*'). */
|
|
39
|
+
export function isFacetId(id) {
|
|
40
|
+
return typeof id === 'string' && id.startsWith('f:');
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// operator.js — operator-identity helpers shared by every no-build domain app
|
|
2
|
+
// pkg (sales/research/content/strategy). SOURCE OF TRUTH — vendored
|
|
3
|
+
// byte-identically into each pkg's dist/lib/operator.js via vendorRuntime()
|
|
4
|
+
// (add 'operator' to that pkg's build.mjs `files` list). Never hand-edit a
|
|
5
|
+
// vendored copy; edit here and re-run the pkg's `scripts/build.mjs`.
|
|
6
|
+
//
|
|
7
|
+
// hostContext.operator is OPTIONAL (see @ikenga/contract's host-context.ts —
|
|
8
|
+
// IkengaHostContextExtensions.operator: OperatorIdentity | undefined): absent
|
|
9
|
+
// means an UNKNOWN operator, not a default one. Every helper below fails safe
|
|
10
|
+
// on a null/undefined operatorId — "mine" matches nothing, "other owner"
|
|
11
|
+
// defaults to true (never mislabels an unclaimed row as the human), and
|
|
12
|
+
// initials degrade to a neutral glyph rather than guessing an identity.
|
|
13
|
+
//
|
|
14
|
+
// This module is host/bridge-agnostic on purpose (pure functions only, no
|
|
15
|
+
// import from bridge.js): callers thread the plain `operatorId` string (or
|
|
16
|
+
// null) down from app.js's `connectBridge()` ctx.operator.id, the same way
|
|
17
|
+
// `activeFeature` is already threaded from ctx.royaltiSuite.activeFeature.
|
|
18
|
+
|
|
19
|
+
/** True only when `value` names the CURRENT known operator. An unknown
|
|
20
|
+
* operator (`operatorId` null/undefined) never matches — a "mine" filter or
|
|
21
|
+
* badge count degrades to empty, never to "everyone" or "no one knows". */
|
|
22
|
+
export function isMine(value, operatorId) {
|
|
23
|
+
return operatorId != null && value === operatorId;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** True when `value` names an owner other than the current operator — the
|
|
27
|
+
* complement used for "agent-tracked" grouping and is-agent avatar styling.
|
|
28
|
+
* An unknown operator makes every named owner read as "other" (we can never
|
|
29
|
+
* be sure an unclaimed value is the human), so this defaults to true rather
|
|
30
|
+
* than false when `operatorId` is null. */
|
|
31
|
+
export function isOtherOwner(value, operatorId) {
|
|
32
|
+
return value != null && value !== operatorId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Single-glyph label for an avatar/initial badge. Falls back to '?' rather
|
|
36
|
+
* than guessing when the label is missing or empty. */
|
|
37
|
+
export function initialOf(label) {
|
|
38
|
+
return typeof label === 'string' && label.length > 0 ? label[0].toUpperCase() : '?';
|
|
39
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// GENERATED by @ikenga/pkg-runtime vendor step — do not edit.
|
|
2
|
+
// Per-pkg identity injected into the shared bridge.js core so the runtime
|
|
3
|
+
// stays byte-identical across pkgs (WP-19). Overwritten on next build.
|
|
4
|
+
export const PKG_ID = "com.ikenga.sales";
|
|
5
|
+
export const LOG_TAG = "sales";
|
package/dist/lib/sales-css.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// GENERATED by scripts/build.mjs from dist/sales.css — do not edit.
|
|
2
2
|
// CSS-as-string: WebKitGTK cannot load link/fetch subresources from the
|
|
3
3
|
// about:srcdoc the shell mounts, so app.js injects this as an inline <style>.
|
|
4
|
-
export default "/* sales.css — domain residue for com.ikenga.sales.\n *\n * Only sales-local classes live here:\n * - .sl-* Forecast view layout (.sl-forecast-kpis, .sl-forecast-kpi, etc.)\n * - .sl-won-* Won view layout (.sl-won-wrap, .sl-won-table, .sl-won-badge)\n * - Kanban + pipeline overrides specific to this domain (.kb-mini-avatar.is-agent,\n * .split-row-when.is-urgent already in kit — no re-derive needed)\n *\n * Kit primitives (.frame*, .ip-split*, .dense-row*, .btn*, .tag, .chip, .badge,\n * .atelier-state*, .nav-group*, .nav-item*, .kb-*, .seg*, .split-*, etc.) are\n * consumed from app-kit-css.js — never re-derived here.\n *\n * Inject order: tokens-css → app-kit-css → sales-css (this file).\n * data-workspace=\"sessions\" on <html> → warm amber-ochre tint for active nav + accents.\n */\n\n/* ── Reduced motion gate ── */\n@media (prefers-reduced-motion: reduce) {\n .split-row { transition: none; }\n .kb-card { transition: none; }\n .kb-card:hover { transform: none; }\n}\n\n/* ── Forecast view (.sl-*) ─────────────────────────────────────────────────── */\n\n.sl-forecast-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-forecast-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-forecast-kpi {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.sl-kpi-k {\n font-family: var(--font-mono, monospace);\n font-size: 10px;\n font-weight: 500;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.sl-kpi-v {\n font-family: var(--font-display, Georgia, serif);\n font-size: 26px;\n font-weight: 500;\n font-style: normal;\n color: var(--fg);\n margin-top: 4px;\n}\n\n.sl-kpi-sub {\n font-size: 0.7rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n.sl-forecast-card {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-forecast-card-h {\n padding: 10px 14px;\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--fg-muted);\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.06));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.sl-funnel-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n}\n\n.sl-funnel-row:last-child {\n border-bottom: none;\n}\n\n.sl-funnel-name {\n width: 100px;\n font-size: 0.78rem;\n color: var(--fg);\n flex-shrink: 0;\n}\n\n.sl-funnel-bar {\n flex: 1;\n height: 22px;\n border-radius: var(--radius-xs, 3px);\n background: var(--bg-sunken, rgba(0,0,0,0.06));\n position: relative;\n overflow: hidden;\n}\n\n.sl-funnel-bar-fill {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--bg-raised, rgba(128,128,128,0.3));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: var(--radius-xs, 3px);\n}\n\n.sl-funnel-bar-wt {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: var(--radius-xs, 3px);\n opacity: 0.9;\n}\n\n.sl-funnel-val {\n width: 72px;\n font-size: 0.72rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n text-align: right;\n flex-shrink: 0;\n}\n\n.sl-months {\n display: flex;\n align-items: flex-end;\n gap: 16px;\n padding: 14px;\n height: 170px;\n}\n\n.sl-month {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n flex: 1;\n}\n\n.sl-month-bar-wrap {\n flex: 1;\n display: flex;\n align-items: flex-end;\n width: 100%;\n}\n\n.sl-month-bar {\n width: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: 3px 3px 0 0;\n opacity: 0.7;\n min-height: 4px;\n}\n\n.sl-month-val {\n font-size: 0.65rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n}\n\n.sl-month-lab {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* ── Won view (.sl-won-*) ───────────────────────────────────────────────────── */\n\n.sl-won-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-won-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-won-table-wrap {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-won-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n}\n\n.sl-won-table thead tr {\n background: var(--bg-inset, rgba(0,0,0,0.04));\n}\n\n.sl-won-table th {\n padding: 8px 12px;\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--fg-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n}\n\n.sl-won-table td {\n padding: 9px 12px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n color: var(--fg);\n vertical-align: middle;\n}\n\n.sl-won-table tbody tr:last-child td {\n border-bottom: none;\n}\n\n.sl-won-table tbody tr:hover td {\n background: var(--bg-hover, rgba(0,0,0,0.025));\n}\n\n.sl-won-badge {\n display: inline-flex;\n align-items: center;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.65rem;\n font-weight: 500;\n background: var(--bg-inset, rgba(0,0,0,0.08));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.02em;\n}\n\n.sl-won-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--live, hsl(150,48%,52%));\n}\n\n/* ── Kanban domain overrides ────────────────────────────────────────────────── */\n\n/* Stage dot colours per the screen doc */\n.kb-col[data-stage=\"lead\"] .kb-col-dot { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.kb-col[data-stage=\"qualified\"] .kb-col-dot { background: var(--systemic, hsl(140,55%,42%)); }\n.kb-col[data-stage=\"proposal\"] .kb-col-dot { background: var(--primary, hsl(215,70%,55%)); }\n.kb-col[data-stage=\"negotiation\"] .kb-col-dot { background: var(--achievement, hsl(38,85%,52%)); }\n.kb-col[data-stage=\"closing\"] .kb-col-dot { background: var(--live, hsl(348,72%,52%)); }\n\n/* Deal row ux-mode dots */\n.ux-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-right: 4px;\n}\n.ux-dot.ux-confirm { background: var(--achievement, hsl(38,85%,52%)); }\n.ux-dot.ux-silent { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.ux-dot.ux-approve { background: var(--primary, hsl(215,70%,55%)); }\n\n/* Next-action chip in list rows */\n.next-chip {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 0.68rem;\n color: var(--fg-muted);\n background: var(--bg-inset, rgba(0,0,0,0.06));\n border-radius: 4px;\n padding: 2px 6px;\n max-width: 180px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Stage chip in detail pane */\n.stage-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.7rem;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 5px;\n background: var(--bg-inset, rgba(0,0,0,0.07));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.03em;\n}\n\n/* Hot filter (Closing soon — achievement tint) */\n.nav-item.is-hot .nav-item-count {\n color: var(--achievement, hsl(38,85%,52%));\n font-weight: 700;\n}\n\n/* Deal amount in list rows */\n.split-row-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--fg);\n white-space: nowrap;\n}\n\n/* Age / urgency */\n.split-row-when {\n font-size: 0.68rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n white-space: nowrap;\n}\n\n.split-row-when.is-urgent {\n color: var(--danger, hsl(0,65%,52%));\n}\n\n/* F-03 · Selected-row accent rail → explicit warm-primary (design uses\n var(--primary), not the sessions amber-ochre --tint-fg-active). */\n.split-row.is-selected .split-row-accent,\n.split-row.is-on .split-row-accent {\n background: var(--primary, hsl(20,50%,34%));\n border-radius: 2px;\n}\n\n/* F-02 · Pipeline list-pane head — sticky Fraunces \"Sales\" title + mono\n count above the stage groups. Ported from the design's .pl-list-head /\n .pl-list-title / .pl-list-meta (not an app-kit primitive). */\n.sl-list-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--space-3, 12px) var(--space-4, 16px);\n border-bottom: 1px solid var(--border-soft);\n position: sticky;\n top: 0;\n background: var(--bg-surface);\n z-index: 6;\n}\n\n.sl-list-title {\n font-family: var(--font-display, Georgia, serif);\n font-weight: 500;\n font-size: var(--text-h3, 18px);\n color: var(--fg);\n}\n\n.sl-list-meta {\n font-family: var(--font-mono, monospace);\n font-size: 10.5px;\n color: var(--fg-faint, var(--fg-muted));\n letter-spacing: 0.04em;\n}\n\n/* F-05 · Detail-pane title → Fraunces 28px (design spec). The app-kit\n primitive sets Fraunces at --text-h2 (24px); sales bumps it to 28px to\n match the locked sales design's .pl-detail-title. */\n.split-detail-title {\n font-family: var(--font-display, Georgia, serif);\n font-size: 28px;\n font-weight: 500;\n line-height: 1.15;\n letter-spacing: -0.012em;\n color: var(--fg);\n margin: 0 0 var(--space-2, 8px);\n}\n\n/* F-06 / F-07 / F-08 · The facts grid (3-col card), next-action card\n (live-tinted border + bg), and next-action head (UPPERCASE mono 10px)\n are all consumed verbatim from the app-kit primitives (.split-facts,\n .split-next, .split-next-head) — no sales-local override. The previous\n 2-col plain-grid / un-tinted overrides were drift and have been removed\n so the kit's design-correct rules apply. */\n\n/* Activity timeline in detail pane */\n.split-timeline {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.split-tl-row {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n}\n\n.split-tl-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--fg-faint, rgba(128,128,128,0.4));\n flex-shrink: 0;\n margin-top: 5px;\n}\n\n.split-tl-body {\n flex: 1;\n}\n\n.split-tl-title {\n font-size: 0.78rem;\n color: var(--fg);\n}\n\n.split-tl-when {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n/* Seg toggle (list/kanban) injected into sidebar nav-group */\n.nav-view-seg {\n margin: 4px 8px 6px;\n}\n\n/* Kanban board horizontal scroll mask */\n.kb-board-wrap {\n overflow-x: auto;\n height: 100%;\n -webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n}\n"
|
|
4
|
+
export default "/* sales.css — domain residue for com.ikenga.sales.\n *\n * Only sales-local classes live here:\n * - .sl-* Forecast view layout (.sl-forecast-kpis, .sl-forecast-kpi, etc.)\n * - .sl-won-* Won view layout (.sl-won-wrap, .sl-won-table, .sl-won-badge)\n * - Kanban + pipeline overrides specific to this domain (.kb-mini-avatar.is-agent,\n * .split-row-when.is-urgent already in kit — no re-derive needed)\n *\n * Kit primitives (.frame*, .ip-split*, .dense-row*, .btn*, .tag, .chip, .badge,\n * .atelier-state*, .nav-group*, .nav-item*, .kb-*, .seg*, .split-*, etc.) are\n * consumed from app-kit-css.js — never re-derived here.\n *\n * Inject order: tokens-css → app-kit-css → sales-css (this file).\n * data-workspace=\"sessions\" on <html> → warm amber-ochre tint for active nav + accents.\n */\n\n/* ── Reduced motion gate ── */\n@media (prefers-reduced-motion: reduce) {\n .split-row { transition: none; }\n .kb-card { transition: none; }\n .kb-card:hover { transform: none; }\n}\n\n/* ── Forecast view (.sl-*) ─────────────────────────────────────────────────── */\n\n.sl-forecast-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-forecast-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-forecast-kpi {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.sl-kpi-k {\n font-family: var(--font-mono, monospace);\n font-size: 10px;\n font-weight: 500;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.08em;\n}\n\n.sl-kpi-v {\n font-family: var(--font-display, Georgia, serif);\n font-size: 26px;\n font-weight: 500;\n font-style: normal;\n color: var(--fg);\n margin-top: 4px;\n}\n\n.sl-kpi-sub {\n font-size: 0.7rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n.sl-forecast-card {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-forecast-card-h {\n padding: 10px 14px;\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--fg-muted);\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.06));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n.sl-funnel-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 14px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n}\n\n.sl-funnel-row:last-child {\n border-bottom: none;\n}\n\n.sl-funnel-name {\n width: 100px;\n font-size: 0.78rem;\n color: var(--fg);\n flex-shrink: 0;\n}\n\n.sl-funnel-bar {\n flex: 1;\n height: 22px;\n border-radius: var(--radius-xs, 3px);\n background: var(--bg-sunken, rgba(0,0,0,0.06));\n position: relative;\n overflow: hidden;\n}\n\n.sl-funnel-bar-fill {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--bg-raised, rgba(128,128,128,0.3));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: var(--radius-xs, 3px);\n}\n\n.sl-funnel-bar-wt {\n position: absolute;\n left: 0;\n top: 0;\n height: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: var(--radius-xs, 3px);\n opacity: 0.9;\n}\n\n.sl-funnel-val {\n width: 72px;\n font-size: 0.72rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n text-align: right;\n flex-shrink: 0;\n}\n\n.sl-months {\n display: flex;\n align-items: flex-end;\n gap: 16px;\n padding: 14px;\n height: 170px;\n}\n\n.sl-month {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 4px;\n flex: 1;\n /* Full-height column so the bar's percentage height resolves against the\n 170px .sl-months container (mirrors the design's .fc-month { height:100% }).\n Completes F-15 — without it the bar-wrap collapses to min-height and the\n JS-percentage bars never scale. */\n height: 100%;\n}\n\n.sl-month-bar-wrap {\n flex: 1;\n display: flex;\n align-items: flex-end;\n width: 100%;\n}\n\n.sl-month-bar {\n width: 100%;\n background: var(--primary, hsl(215,70%,55%));\n border-radius: 3px 3px 0 0;\n opacity: 0.7;\n min-height: 4px;\n}\n\n.sl-month-val {\n font-size: 0.65rem;\n font-family: var(--font-mono, monospace);\n color: var(--fg-muted);\n}\n\n.sl-month-lab {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n\n/* ── Won view (.sl-won-*) ───────────────────────────────────────────────────── */\n\n.sl-won-wrap {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 16px;\n overflow-y: auto;\n height: 100%;\n}\n\n.sl-won-kpis {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 10px;\n}\n\n.sl-won-table-wrap {\n background: var(--bg-surface, var(--bg-base));\n border: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n border-radius: 8px;\n overflow: hidden;\n}\n\n.sl-won-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.8rem;\n}\n\n.sl-won-table thead tr {\n background: var(--bg-inset, rgba(0,0,0,0.04));\n}\n\n.sl-won-table th {\n padding: 8px 12px;\n text-align: left;\n font-size: 0.7rem;\n font-weight: 600;\n color: var(--fg-muted);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.08));\n}\n\n.sl-won-table td {\n padding: 9px 12px;\n border-bottom: 1px solid var(--border-soft, rgba(0,0,0,0.04));\n color: var(--fg);\n vertical-align: middle;\n}\n\n.sl-won-table tbody tr:last-child td {\n border-bottom: none;\n}\n\n.sl-won-table tbody tr:hover td {\n background: var(--bg-hover, rgba(0,0,0,0.025));\n}\n\n.sl-won-badge {\n display: inline-flex;\n align-items: center;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.65rem;\n font-weight: 500;\n background: var(--bg-inset, rgba(0,0,0,0.08));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.02em;\n}\n\n.sl-won-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.8rem;\n font-weight: 600;\n color: var(--live, hsl(150,48%,52%));\n}\n\n/* ── Kanban domain overrides ────────────────────────────────────────────────── */\n\n/* Stage dot colours per the screen doc */\n.kb-col[data-stage=\"lead\"] .kb-col-dot { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.kb-col[data-stage=\"qualified\"] .kb-col-dot { background: var(--systemic, hsl(140,55%,42%)); }\n.kb-col[data-stage=\"proposal\"] .kb-col-dot { background: var(--primary, hsl(215,70%,55%)); }\n.kb-col[data-stage=\"negotiation\"] .kb-col-dot { background: var(--achievement, hsl(38,85%,52%)); }\n.kb-col[data-stage=\"closing\"] .kb-col-dot { background: var(--live, hsl(348,72%,52%)); }\n\n/* Deal row ux-mode dots */\n.ux-dot {\n display: inline-block;\n width: 7px;\n height: 7px;\n border-radius: 50%;\n flex-shrink: 0;\n margin-right: 4px;\n}\n.ux-dot.ux-confirm { background: var(--achievement, hsl(38,85%,52%)); }\n.ux-dot.ux-silent { background: var(--fg-faint, rgba(128,128,128,0.4)); }\n.ux-dot.ux-approve { background: var(--primary, hsl(215,70%,55%)); }\n\n/* Next-action chip in list rows */\n.next-chip {\n display: inline-flex;\n align-items: center;\n gap: 3px;\n font-size: 0.68rem;\n color: var(--fg-muted);\n background: var(--bg-inset, rgba(0,0,0,0.06));\n border-radius: 4px;\n padding: 2px 6px;\n max-width: 180px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* Stage chip in detail pane */\n.stage-chip {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.7rem;\n font-weight: 600;\n padding: 3px 8px;\n border-radius: 5px;\n background: var(--bg-inset, rgba(0,0,0,0.07));\n color: var(--fg-muted);\n text-transform: capitalize;\n letter-spacing: 0.03em;\n}\n\n/* Hot filter (Closing soon — achievement tint) */\n.nav-item.is-hot .nav-item-count {\n color: var(--achievement, hsl(38,85%,52%));\n font-weight: 700;\n}\n\n/* Deal amount in list rows */\n.split-row-amt {\n font-family: var(--font-mono, monospace);\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--fg);\n white-space: nowrap;\n}\n\n/* Age / urgency */\n.split-row-when {\n font-size: 0.68rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n white-space: nowrap;\n}\n\n.split-row-when.is-urgent {\n color: var(--danger, hsl(0,65%,52%));\n}\n\n/* F-03 · Selected-row accent rail → explicit warm-primary (design uses\n var(--primary), not the sessions amber-ochre --tint-fg-active). */\n.split-row.is-selected .split-row-accent,\n.split-row.is-on .split-row-accent {\n background: var(--primary, hsl(20,50%,34%));\n border-radius: 2px;\n}\n\n/* F-02 · Pipeline list-pane head — sticky Fraunces \"Sales\" title + mono\n count above the stage groups. Ported from the design's .pl-list-head /\n .pl-list-title / .pl-list-meta (not an app-kit primitive). */\n.sl-list-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: var(--space-3, 12px) var(--space-4, 16px);\n border-bottom: 1px solid var(--border-soft);\n position: sticky;\n top: 0;\n background: var(--bg-surface);\n z-index: 6;\n}\n\n.sl-list-title {\n font-family: var(--font-display, Georgia, serif);\n font-weight: 500;\n font-size: var(--text-h3, 18px);\n color: var(--fg);\n}\n\n.sl-list-meta {\n font-family: var(--font-mono, monospace);\n font-size: 10.5px;\n color: var(--fg-faint, var(--fg-muted));\n letter-spacing: 0.04em;\n}\n\n/* F-05 · Detail-pane title → Fraunces 28px (design spec). The app-kit\n primitive sets Fraunces at --text-h2 (24px); sales bumps it to 28px to\n match the locked sales design's .pl-detail-title. */\n.split-detail-title {\n font-family: var(--font-display, Georgia, serif);\n font-size: 28px;\n font-weight: 500;\n line-height: 1.15;\n letter-spacing: -0.012em;\n color: var(--fg);\n margin: 0 0 var(--space-2, 8px);\n}\n\n/* F-06 / F-07 / F-08 · The facts grid (3-col card), next-action card\n (live-tinted border + bg), and next-action head (UPPERCASE mono 10px)\n are all consumed verbatim from the app-kit primitives (.split-facts,\n .split-next, .split-next-head) — no sales-local override. The previous\n 2-col plain-grid / un-tinted overrides were drift and have been removed\n so the kit's design-correct rules apply. */\n\n/* Activity timeline in detail pane */\n.split-timeline {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.split-tl-row {\n display: flex;\n gap: 10px;\n align-items: flex-start;\n}\n\n.split-tl-dot {\n width: 7px;\n height: 7px;\n border-radius: 50%;\n background: var(--fg-faint, rgba(128,128,128,0.4));\n flex-shrink: 0;\n margin-top: 5px;\n}\n\n.split-tl-body {\n flex: 1;\n}\n\n.split-tl-title {\n font-size: 0.78rem;\n color: var(--fg);\n}\n\n.split-tl-when {\n font-size: 0.65rem;\n color: var(--fg-faint, var(--fg-muted));\n font-family: var(--font-mono, monospace);\n}\n\n/* Seg toggle (list/kanban) injected into sidebar nav-group */\n.nav-view-seg {\n margin: 4px 8px 6px;\n}\n\n/* Kanban board horizontal scroll mask */\n.kb-board-wrap {\n overflow-x: auto;\n height: 100%;\n -webkit-mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n mask-image: linear-gradient(to right, black calc(100% - 40px), transparent 100%);\n}\n"
|
package/dist/lib/ui.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// React + htm via esm.sh — no JSX transpile needed. Forkers edit JS, reload.
|
|
2
2
|
//
|
|
3
|
+
// SOURCE OF TRUTH (WP-19). This is the single, byte-identical UI runtime vendored
|
|
4
|
+
// into every no-build app pkg's dist/lib/ui.js. It supersedes the ~80%-shared
|
|
5
|
+
// per-pkg copies; the only thing that used to diverge was each pkg's slice of the
|
|
6
|
+
// icon dictionary, so this file carries the UNION of every pkg's glyphs (57) — a
|
|
7
|
+
// pkg simply ignores the names it never references. Never hand-edit a vendored
|
|
8
|
+
// copy; edit here and re-run the pkg's scripts/build.mjs.
|
|
9
|
+
//
|
|
3
10
|
// htm uses tagged template literals to render React elements. Same mental model
|
|
4
11
|
// as JSX (component tags, expressions in ${}), just without a build step.
|
|
5
12
|
|
|
@@ -38,30 +45,66 @@ export {
|
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
// Tiny icon helper — lucide-static SVG paths inlined as needed to avoid an
|
|
41
|
-
// extra CDN hop (the source
|
|
42
|
-
// pattern instead).
|
|
48
|
+
// extra CDN hop (the source apps used lucide-react; we mirror suite's inline
|
|
49
|
+
// pattern instead). WP-19 UNION of every pkg's glyphs; add more as features need.
|
|
43
50
|
const ICONS = {
|
|
44
|
-
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
51
|
+
activity: "M22 12h-4l-3 9L9 3l-3 9H2",
|
|
52
|
+
'alert-circle': "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 8v4M12 16h.01",
|
|
53
|
+
'alert-triangle': "M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01",
|
|
54
|
+
archive: "M21 8v13H3V8M1 3h22v5H1zM10 12h4",
|
|
55
|
+
'arrow-down': "M12 5v14M19 12l-7 7-7-7",
|
|
56
|
+
'arrow-left': "M19 12H5M12 5l-7 7 7 7",
|
|
57
|
+
'arrow-right': "M5 12h14M12 5l7 7-7 7",
|
|
58
|
+
'bar-chart-2': "M18 20V10M12 20V4M6 20v-6",
|
|
59
|
+
'book-open': "M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z",
|
|
60
|
+
broom: "M9.8 12.2 5 17l3 3 4.8-4.8M14 8l-3.5 3.5 4 4L18 12l-4-4z M14 8l5-5M19 3l2 2",
|
|
61
|
+
calendar: "M3 4h18v18H3V4zM16 2v4M8 2v4M3 10h18",
|
|
62
|
+
'calendar-days': "M8 2v4M16 2v4M3 10h18M5 4h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM8 14h.01M12 14h.01M16 14h.01M8 18h.01M12 18h.01M16 18h.01",
|
|
63
|
+
check: "M20 6L9 17l-5-5",
|
|
64
|
+
'check-check': "M18 6 7 17l-5-5M22 10l-7.5 7.5L13 16",
|
|
65
|
+
'check-circle': "M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4L12 14.01l-3-3",
|
|
66
|
+
'check-square': "M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11",
|
|
67
|
+
'chevron-down': "M6 9l6 6 6-6",
|
|
68
|
+
'chevron-left': "M15 18l-6-6 6-6",
|
|
69
|
+
'chevron-right': "M9 18l6-6-6-6",
|
|
70
|
+
'chevron-up': "M18 15l-6-6-6 6",
|
|
71
|
+
clock: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 6v6l4 2",
|
|
72
|
+
'corner-down-left': "M9 10l-5 5 5 5M4 15h7a4 4 0 0 0 4-4V5",
|
|
73
|
+
'edit-3': "M12 20h9M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z",
|
|
74
|
+
eye: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z",
|
|
75
|
+
'file-text': "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8",
|
|
76
|
+
filter: "M22 3H2l8 9.46V19l4 2v-8.54L22 3z",
|
|
77
|
+
'git-branch': "M6 3v12M18 9a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 21a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 9a9 9 0 0 1-9 9",
|
|
78
|
+
globe: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z",
|
|
79
|
+
inbox: "M22 12h-6l-2 3H10l-2-3H2M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z",
|
|
80
|
+
linkedin: "M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6zM2 9h4v12H2zM4 6a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",
|
|
81
|
+
list: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01",
|
|
82
|
+
loader: "M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83",
|
|
83
|
+
mail: "M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6",
|
|
84
|
+
'more-horizontal': "M12 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM19 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM5 13a1 1 0 1 0 0-2 1 1 0 0 0 0 2z",
|
|
85
|
+
pause: "M6 4h4v16H6zM14 4h4v16h-4z",
|
|
86
|
+
people: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75",
|
|
87
|
+
'play-circle': "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM10 8l6 4-6 4V8z",
|
|
88
|
+
plus: "M5 12h14M12 5v14",
|
|
89
|
+
radio: "M12 12m-2 0a2 2 0 1 0 4 0a2 2 0 1 0-4 0M4.93 4.93a10 10 0 0 0 0 14.14M19.07 4.93a10 10 0 0 1 0 14.14M7.76 7.76a6 6 0 0 0 0 8.49M16.24 7.76a6 6 0 0 1 0 8.49",
|
|
90
|
+
'refresh-cw': "M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15",
|
|
91
|
+
search: "M11 17.5a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13zM21 21l-4.35-4.35",
|
|
92
|
+
send: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z",
|
|
93
|
+
share2: "M8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98M21 5a3 3 0 1 0-6 0 3 3 0 0 0 6 0zM9 12a3 3 0 1 0-6 0 3 3 0 0 0 6 0zM21 19a3 3 0 1 0-6 0 3 3 0 0 0 6 0z",
|
|
94
|
+
'skip-forward': "M5 4l10 8-10 8V4zM19 5v14",
|
|
95
|
+
star: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z",
|
|
96
|
+
stethoscope: "M11 2v2M5 2v2M5 3H4a2 2 0 0 0-2 2v4a6 6 0 0 0 12 0V5a2 2 0 0 0-2-2h-1M8 15a6 6 0 0 0 12 0v-3M20 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4z",
|
|
97
|
+
tag: "M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01",
|
|
98
|
+
terminal: "M4 17l6-6-6-6M12 19h8",
|
|
99
|
+
timer: "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM12 6v6M16.24 16.24l-4.24-4.24M4.93 4.93l14.14 14.14",
|
|
100
|
+
trash: "M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6",
|
|
101
|
+
'trending-up': "M23 6l-9.5 9.5-5-5L1 18M17 6h6v6",
|
|
102
|
+
twitter: "M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z",
|
|
103
|
+
user: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z",
|
|
104
|
+
users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75",
|
|
105
|
+
x: "M18 6L6 18M6 6l12 12",
|
|
106
|
+
'x-circle': "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zM15 9l-6 6M9 9l6 6",
|
|
107
|
+
zap: "M13 2L3 14h9l-1 8 10-12h-9l1-8z",
|
|
65
108
|
};
|
|
66
109
|
|
|
67
110
|
// Class-name joiner — replaces clsx + tailwind-merge. With Tailwind gone there
|
|
@@ -80,7 +123,7 @@ export function cn(...inputs) {
|
|
|
80
123
|
}
|
|
81
124
|
|
|
82
125
|
// Button — ported from src/components/Button.tsx. Tailwind utility classes
|
|
83
|
-
// became .tk-btn / sz-* / v-* rules in
|
|
126
|
+
// became .tk-btn / sz-* / v-* rules in the pkg's domain CSS.
|
|
84
127
|
export function Button({ variant = 'default', size = 'md', class: cls = '', children, ...props }) {
|
|
85
128
|
const className = ['tk-btn', `sz-${size}`, `v-${variant}`, cls]
|
|
86
129
|
.filter(Boolean)
|
|
@@ -105,3 +148,30 @@ export function Icon({ name, size = 16, className, strokeWidth = 2 }) {
|
|
|
105
148
|
stroke-linejoin="round"
|
|
106
149
|
><path d=${path} /></svg>`;
|
|
107
150
|
}
|
|
151
|
+
|
|
152
|
+
// Auto-growing textarea hook — the body-editor pattern for outbox/detail panes.
|
|
153
|
+
// A fixed-height textarea with an inner scrollbar reads as a cramped form
|
|
154
|
+
// field; content-sized, it reads as a document the pane scrolls naturally
|
|
155
|
+
// (the outer pane owns overflow). Sets height to scrollHeight on mount, on
|
|
156
|
+
// value change, and on container resize; CSS `field-sizing: content` will
|
|
157
|
+
// obsolete this once WebKitGTK ships it.
|
|
158
|
+
// const ref = useAutoGrow(value);
|
|
159
|
+
// html`<textarea ref=${ref} value=${value} ... style=${{ overflow: 'hidden', resize: 'none' }}></textarea>`
|
|
160
|
+
export function useAutoGrow(value, { minHeight = 160 } = {}) {
|
|
161
|
+
const ref = React.useRef(null);
|
|
162
|
+
const fit = React.useCallback(() => {
|
|
163
|
+
const el = ref.current;
|
|
164
|
+
if (!el) return;
|
|
165
|
+
el.style.height = 'auto';
|
|
166
|
+
el.style.height = `${Math.max(minHeight, el.scrollHeight)}px`;
|
|
167
|
+
}, [minHeight]);
|
|
168
|
+
React.useLayoutEffect(fit, [fit, value]);
|
|
169
|
+
React.useEffect(() => {
|
|
170
|
+
const el = ref.current;
|
|
171
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
172
|
+
const ro = new ResizeObserver(fit);
|
|
173
|
+
ro.observe(el.parentElement ?? el);
|
|
174
|
+
return () => ro.disconnect();
|
|
175
|
+
}, [fit]);
|
|
176
|
+
return ref;
|
|
177
|
+
}
|
package/dist/sales.css
CHANGED
|
@@ -160,6 +160,11 @@
|
|
|
160
160
|
align-items: center;
|
|
161
161
|
gap: 4px;
|
|
162
162
|
flex: 1;
|
|
163
|
+
/* Full-height column so the bar's percentage height resolves against the
|
|
164
|
+
170px .sl-months container (mirrors the design's .fc-month { height:100% }).
|
|
165
|
+
Completes F-15 — without it the bar-wrap collapses to min-height and the
|
|
166
|
+
JS-percentage bars never scale. */
|
|
167
|
+
height: 100%;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
.sl-month-bar-wrap {
|
package/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "com.ikenga.sales",
|
|
3
3
|
"name": "Sales",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"ikenga_api": "1",
|
|
6
6
|
"kind": "embedded",
|
|
7
7
|
"author": { "name": "Royalti", "key": "royalti" },
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
"sales_lead_scores",
|
|
48
48
|
"contacts"
|
|
49
49
|
],
|
|
50
|
-
"vault.keys": []
|
|
50
|
+
"vault.keys": [],
|
|
51
|
+
"engine": ["invoke"]
|
|
51
52
|
},
|
|
52
53
|
|
|
53
54
|
"requires": [
|
package/package.json
CHANGED