@ponchia/ui 0.4.1 → 0.5.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +230 -8
  2. package/MIGRATIONS.json +92 -0
  3. package/README.md +9 -6
  4. package/annotations/index.d.ts +280 -0
  5. package/annotations/index.js +522 -0
  6. package/behaviors/carousel.js +197 -0
  7. package/behaviors/combobox.js +195 -0
  8. package/behaviors/command.js +187 -0
  9. package/behaviors/connectors.js +96 -0
  10. package/behaviors/crosshair.js +58 -0
  11. package/behaviors/dialog.js +73 -0
  12. package/behaviors/disclosure.js +25 -0
  13. package/behaviors/dismissible.js +24 -0
  14. package/behaviors/forms.js +158 -0
  15. package/behaviors/glyph.js +109 -0
  16. package/behaviors/index.d.ts +79 -0
  17. package/behaviors/index.js +18 -1409
  18. package/behaviors/internal.js +50 -0
  19. package/behaviors/legend.js +46 -0
  20. package/behaviors/menu.js +46 -0
  21. package/behaviors/popover.js +108 -0
  22. package/behaviors/spotlight.js +53 -0
  23. package/behaviors/table.js +109 -0
  24. package/behaviors/tabs.js +103 -0
  25. package/behaviors/theme.js +82 -0
  26. package/behaviors/toast.js +152 -0
  27. package/classes/index.d.ts +280 -2
  28. package/classes/index.js +313 -2
  29. package/connectors/index.d.ts +71 -0
  30. package/connectors/index.js +179 -0
  31. package/css/analytical.css +21 -0
  32. package/css/annotations.css +292 -0
  33. package/css/command.css +97 -0
  34. package/css/connectors.css +93 -0
  35. package/css/crosshair.css +100 -0
  36. package/css/feedback.css +51 -0
  37. package/css/fonts.css +11 -7
  38. package/css/generated.css +117 -0
  39. package/css/legend.css +268 -0
  40. package/css/marks.css +144 -0
  41. package/css/primitives.css +18 -0
  42. package/css/report.css +12 -31
  43. package/css/selection.css +46 -0
  44. package/css/sources.css +179 -0
  45. package/css/spotlight.css +104 -0
  46. package/css/state.css +121 -0
  47. package/css/tokens.css +25 -37
  48. package/css/workbench.css +83 -0
  49. package/dist/bronto.css +1 -1
  50. package/dist/css/analytical.css +1 -0
  51. package/dist/css/annotations.css +1 -0
  52. package/dist/css/command.css +1 -0
  53. package/dist/css/connectors.css +1 -0
  54. package/dist/css/crosshair.css +1 -0
  55. package/dist/css/feedback.css +1 -1
  56. package/dist/css/fonts.css +1 -1
  57. package/dist/css/generated.css +1 -0
  58. package/dist/css/legend.css +1 -0
  59. package/dist/css/marks.css +1 -0
  60. package/dist/css/primitives.css +1 -1
  61. package/dist/css/report.css +1 -1
  62. package/dist/css/selection.css +1 -0
  63. package/dist/css/sources.css +1 -0
  64. package/dist/css/spotlight.css +1 -0
  65. package/dist/css/state.css +1 -0
  66. package/dist/css/workbench.css +1 -0
  67. package/docs/adr/0003-theme-model.md +7 -4
  68. package/docs/annotations.md +345 -0
  69. package/docs/architecture.md +202 -0
  70. package/docs/command.md +95 -0
  71. package/docs/connectors.md +91 -0
  72. package/docs/crosshair.md +63 -0
  73. package/docs/generated.md +91 -0
  74. package/docs/legends.md +168 -0
  75. package/docs/marks.md +86 -0
  76. package/docs/reference.md +309 -3
  77. package/docs/reporting.md +49 -14
  78. package/docs/selection.md +40 -0
  79. package/docs/sources.md +110 -0
  80. package/docs/spotlight.md +78 -0
  81. package/docs/stability.md +16 -1
  82. package/docs/state.md +85 -0
  83. package/docs/usage.md +22 -0
  84. package/docs/workbench.md +72 -0
  85. package/fonts/doto-400.woff2 +0 -0
  86. package/fonts/doto-500.woff2 +0 -0
  87. package/fonts/doto-600.woff2 +0 -0
  88. package/fonts/doto-700.woff2 +0 -0
  89. package/fonts/doto-800.woff2 +0 -0
  90. package/fonts/doto-900.woff2 +0 -0
  91. package/llms.txt +229 -6
  92. package/package.json +69 -4
  93. package/qwik/index.d.ts +5 -0
  94. package/qwik/index.js +20 -0
  95. package/react/index.d.ts +5 -0
  96. package/react/index.js +10 -0
  97. package/solid/index.d.ts +5 -0
  98. package/solid/index.js +10 -0
  99. package/tokens/index.js +9 -5
  100. package/fonts/doto-400.ttf +0 -0
  101. package/fonts/doto-500.ttf +0 -0
  102. package/fonts/doto-600.ttf +0 -0
  103. package/fonts/doto-700.ttf +0 -0
  104. package/fonts/doto-800.ttf +0 -0
  105. package/fonts/doto-900.ttf +0 -0
package/classes/index.js CHANGED
@@ -268,8 +268,6 @@ export const cls = Object.freeze({
268
268
  reportAppendix: 'ui-report__appendix',
269
269
  reportFootnotes: 'ui-report__footnotes',
270
270
  chart: 'ui-chart',
271
- chartLegend: 'ui-chart__legend',
272
- chartSwatch: 'ui-chart__swatch',
273
271
  chartCaption: 'ui-chart__caption',
274
272
  chartPlot: 'ui-chart__plot',
275
273
  chartBar: 'ui-chart__bar',
@@ -277,6 +275,188 @@ export const cls = Object.freeze({
277
275
  chartTrack: 'ui-chart__track',
278
276
  chartFill: 'ui-chart__fill',
279
277
  chartFallback: 'ui-chart__fallback',
278
+ // legend (standalone data keys — css/legend.css)
279
+ legend: 'ui-legend',
280
+ legendVertical: 'ui-legend--vertical',
281
+ legendCompact: 'ui-legend--compact',
282
+ legendGradient: 'ui-legend--gradient',
283
+ legendDiverging: 'ui-legend--diverging',
284
+ legendThreshold: 'ui-legend--threshold',
285
+ legendWithValues: 'ui-legend--with-values',
286
+ legendInteractive: 'ui-legend--interactive',
287
+ legendTitle: 'ui-legend__title',
288
+ legendItem: 'ui-legend__item',
289
+ legendSwatch: 'ui-legend__swatch',
290
+ legendSwatchCircle: 'ui-legend__swatch--circle',
291
+ legendSwatchLine: 'ui-legend__swatch--line',
292
+ legendSwatch1: 'ui-legend__swatch--1',
293
+ legendSwatch2: 'ui-legend__swatch--2',
294
+ legendSwatch3: 'ui-legend__swatch--3',
295
+ legendSwatch4: 'ui-legend__swatch--4',
296
+ legendSwatch5: 'ui-legend__swatch--5',
297
+ legendSwatch6: 'ui-legend__swatch--6',
298
+ legendSwatch7: 'ui-legend__swatch--7',
299
+ legendSwatch8: 'ui-legend__swatch--8',
300
+ legendSymbol: 'ui-legend__symbol',
301
+ legendLabel: 'ui-legend__label',
302
+ legendValue: 'ui-legend__value',
303
+ legendCaption: 'ui-legend__caption',
304
+ legendTrack: 'ui-legend__track',
305
+ legendTicks: 'ui-legend__ticks',
306
+ legendTick: 'ui-legend__tick',
307
+ annotation: 'ui-annotation',
308
+ annotationSubject: 'ui-annotation__subject',
309
+ annotationConnector: 'ui-annotation__connector',
310
+ annotationConnectorEnd: 'ui-annotation__connector-end',
311
+ annotationNote: 'ui-annotation__note',
312
+ annotationNoteLine: 'ui-annotation__note-line',
313
+ annotationTitle: 'ui-annotation__title',
314
+ annotationLabel: 'ui-annotation__label',
315
+ annotationBadge: 'ui-annotation__badge',
316
+ annotationLabelVariant: 'ui-annotation--label',
317
+ annotationCallout: 'ui-annotation--callout',
318
+ annotationElbow: 'ui-annotation--elbow',
319
+ annotationCurve: 'ui-annotation--curve',
320
+ annotationCircle: 'ui-annotation--circle',
321
+ annotationRect: 'ui-annotation--rect',
322
+ annotationThreshold: 'ui-annotation--threshold',
323
+ annotationBadgeVariant: 'ui-annotation--badge',
324
+ annotationBracket: 'ui-annotation--bracket',
325
+ annotationBand: 'ui-annotation--band',
326
+ annotationSlope: 'ui-annotation--slope',
327
+ annotationCompare: 'ui-annotation--compare',
328
+ annotationCluster: 'ui-annotation--cluster',
329
+ annotationAxis: 'ui-annotation--axis',
330
+ annotationTimeline: 'ui-annotation--timeline',
331
+ annotationEvidence: 'ui-annotation--evidence',
332
+ annotationAccent: 'ui-annotation--accent',
333
+ annotationMuted: 'ui-annotation--muted',
334
+ annotationSuccess: 'ui-annotation--success',
335
+ annotationWarning: 'ui-annotation--warning',
336
+ annotationDanger: 'ui-annotation--danger',
337
+ annotationInfo: 'ui-annotation--info',
338
+ annotationDraw: 'ui-annotation--draw',
339
+ annotationPulse: 'ui-annotation--pulse',
340
+ annotationReveal: 'ui-annotation--reveal',
341
+ annotationFocus: 'ui-annotation--focus',
342
+ // marks (evidence/emphasis for running text — css/marks.css)
343
+ mark: 'ui-mark',
344
+ markAccent: 'ui-mark--accent',
345
+ markSuccess: 'ui-mark--success',
346
+ markWarning: 'ui-mark--warning',
347
+ markDanger: 'ui-mark--danger',
348
+ markInfo: 'ui-mark--info',
349
+ markMuted: 'ui-mark--muted',
350
+ markUnderline: 'ui-mark--underline',
351
+ markBox: 'ui-mark--box',
352
+ markStrike: 'ui-mark--strike',
353
+ markDraw: 'ui-mark--draw',
354
+ bracketNote: 'ui-bracket-note',
355
+ bracketNoteLabel: 'ui-bracket-note__label',
356
+ bracketNoteAccent: 'ui-bracket-note--accent',
357
+ bracketNoteWarning: 'ui-bracket-note--warning',
358
+ bracketNoteDanger: 'ui-bracket-note--danger',
359
+ bracketNoteInfo: 'ui-bracket-note--info',
360
+ // connectors (leader lines — css/connectors.css)
361
+ connector: 'ui-connector',
362
+ connectorPath: 'ui-connector__path',
363
+ connectorEnd: 'ui-connector__end',
364
+ connectorDashed: 'ui-connector--dashed',
365
+ connectorAccent: 'ui-connector--accent',
366
+ connectorMuted: 'ui-connector--muted',
367
+ connectorSuccess: 'ui-connector--success',
368
+ connectorWarning: 'ui-connector--warning',
369
+ connectorDanger: 'ui-connector--danger',
370
+ connectorInfo: 'ui-connector--info',
371
+ connectorDraw: 'ui-connector--draw',
372
+ // spotlight (guided-focus overlay — css/spotlight.css)
373
+ spotlight: 'ui-spotlight',
374
+ spotlightHole: 'ui-spotlight__hole',
375
+ spotlightRing: 'ui-spotlight--ring',
376
+ tourNote: 'ui-tour-note',
377
+ tourNoteStep: 'ui-tour-note__step',
378
+ tourNoteTitle: 'ui-tour-note__title',
379
+ tourNoteBody: 'ui-tour-note__body',
380
+ tourNoteActions: 'ui-tour-note__actions',
381
+ // crosshair (plot ruler + readout — css/crosshair.css)
382
+ crosshair: 'ui-crosshair',
383
+ crosshairMuted: 'ui-crosshair--muted',
384
+ crosshairLine: 'ui-crosshair__line',
385
+ crosshairLineX: 'ui-crosshair__line--x',
386
+ crosshairLineY: 'ui-crosshair__line--y',
387
+ crosshairBadge: 'ui-crosshair__badge',
388
+ readout: 'ui-readout',
389
+ // selection-state vocabulary (cross-cutting — css/selection.css)
390
+ sel: 'ui-sel',
391
+ selOn: 'ui-sel--on',
392
+ selOff: 'ui-sel--off',
393
+ selMaybe: 'ui-sel--maybe',
394
+ // source / citation / provenance — the trust layer (css/sources.css)
395
+ citation: 'ui-citation',
396
+ citationChip: 'ui-citation--chip',
397
+ sourceList: 'ui-source-list',
398
+ sourceListItem: 'ui-source-list__item',
399
+ sourceCard: 'ui-source-card',
400
+ sourceCardTitle: 'ui-source-card__title',
401
+ sourceCardOrigin: 'ui-source-card__origin',
402
+ sourceCardTime: 'ui-source-card__time',
403
+ sourceCardExcerpt: 'ui-source-card__excerpt',
404
+ sourceCardActions: 'ui-source-card__actions',
405
+ provenance: 'ui-provenance',
406
+ provenanceItem: 'ui-provenance__item',
407
+ srcVerified: 'ui-src--verified',
408
+ srcUnverified: 'ui-src--unverified',
409
+ srcGenerated: 'ui-src--generated',
410
+ srcReviewed: 'ui-src--reviewed',
411
+ srcStale: 'ui-src--stale',
412
+ srcConflict: 'ui-src--conflict',
413
+ // lifecycle / system state (css/state.css)
414
+ state: 'ui-state',
415
+ stateLabel: 'ui-state__label',
416
+ stateDetail: 'ui-state__detail',
417
+ stateBusy: 'ui-state--busy',
418
+ stateSaving: 'ui-state--saving',
419
+ stateSaved: 'ui-state--saved',
420
+ stateQueued: 'ui-state--queued',
421
+ stateOffline: 'ui-state--offline',
422
+ stateStale: 'ui-state--stale',
423
+ stateConflict: 'ui-state--conflict',
424
+ stateError: 'ui-state--error',
425
+ stateLocked: 'ui-state--locked',
426
+ stateReviewed: 'ui-state--reviewed',
427
+ stateNeedsReview: 'ui-state--needs-review',
428
+ syncbar: 'ui-syncbar',
429
+ // generated content / AI-trust surfaces (css/generated.css)
430
+ generated: 'ui-generated',
431
+ generatedLabel: 'ui-generated__label',
432
+ originLabel: 'ui-origin-label',
433
+ originLabelAi: 'ui-origin-label--ai',
434
+ reasoning: 'ui-reasoning',
435
+ reasoningBody: 'ui-reasoning__body',
436
+ toolLog: 'ui-tool-log',
437
+ toolCall: 'ui-tool-call',
438
+ toolCallName: 'ui-tool-call__name',
439
+ toolCallStatus: 'ui-tool-call__status',
440
+ toolCallBody: 'ui-tool-call__body',
441
+ // workbench — inspector / property / selection bar (css/workbench.css)
442
+ inspector: 'ui-inspector',
443
+ inspectorHeader: 'ui-inspector__header',
444
+ inspectorBody: 'ui-inspector__body',
445
+ property: 'ui-property',
446
+ propertyLabel: 'ui-property__label',
447
+ propertyValue: 'ui-property__value',
448
+ selectionbar: 'ui-selectionbar',
449
+ selectionbarCount: 'ui-selectionbar__count',
450
+ selectionbarActions: 'ui-selectionbar__actions',
451
+ // command palette shell (css/command.css + initCommand)
452
+ command: 'ui-command',
453
+ commandInput: 'ui-command__input',
454
+ commandList: 'ui-command__list',
455
+ commandGroup: 'ui-command__group',
456
+ commandItem: 'ui-command__item',
457
+ commandShortcut: 'ui-command__shortcut',
458
+ commandMeta: 'ui-command__meta',
459
+ commandEmpty: 'ui-command__empty',
280
460
  printOnly: 'ui-print-only',
281
461
  screenOnly: 'ui-screen-only',
282
462
  breakBefore: 'ui-break-before',
@@ -284,6 +464,8 @@ export const cls = Object.freeze({
284
464
  keep: 'ui-keep',
285
465
  printExact: 'ui-print-exact',
286
466
  kbd: 'ui-kbd',
467
+ shortcut: 'ui-shortcut',
468
+ shortcutSep: 'ui-shortcut__sep',
287
469
  display: 'ui-display',
288
470
  mono: 'ui-mono',
289
471
  muted: 'ui-muted',
@@ -345,6 +527,31 @@ export function cx(...parts) {
345
527
 
346
528
  const j = (...p) => p.filter(Boolean).join(' ');
347
529
 
530
+ // Lifecycle state → canonical tone class.
531
+ const stateTone = (state) =>
532
+ ({
533
+ saving: cls.stateSaving,
534
+ saved: cls.stateSaved,
535
+ queued: cls.stateQueued,
536
+ offline: cls.stateOffline,
537
+ stale: cls.stateStale,
538
+ conflict: cls.stateConflict,
539
+ error: cls.stateError,
540
+ locked: cls.stateLocked,
541
+ reviewed: cls.stateReviewed,
542
+ 'needs-review': cls.stateNeedsReview,
543
+ })[state] || '';
544
+
545
+ // Trust-state → tone class, shared by the source/citation/provenance recipes.
546
+ const srcTone = (state) =>
547
+ (state === 'verified' && cls.srcVerified) ||
548
+ (state === 'unverified' && cls.srcUnverified) ||
549
+ (state === 'generated' && cls.srcGenerated) ||
550
+ (state === 'reviewed' && cls.srcReviewed) ||
551
+ (state === 'stale' && cls.srcStale) ||
552
+ (state === 'conflict' && cls.srcConflict) ||
553
+ '';
554
+
348
555
  export const ui = {
349
556
  button: ({ variant, icon, size } = {}) =>
350
557
  j(
@@ -440,6 +647,110 @@ export const ui = {
440
647
  j(cls.container, narrow && cls.containerNarrow, wide && cls.containerWide),
441
648
  tag: ({ accent } = {}) => j(cls.tag, accent && cls.tagAccent),
442
649
  inputIcon: ({ end } = {}) => j(cls.inputIcon, end && cls.inputIconEnd),
650
+ legend: ({ orient, type, diverging, compact, withValues, interactive } = {}) =>
651
+ j(
652
+ cls.legend,
653
+ orient === 'vertical' && cls.legendVertical,
654
+ type === 'gradient' && cls.legendGradient,
655
+ type === 'threshold' && cls.legendThreshold,
656
+ diverging && cls.legendDiverging,
657
+ compact && cls.legendCompact,
658
+ withValues && cls.legendWithValues,
659
+ interactive && cls.legendInteractive,
660
+ ),
661
+ legendItem: ({ inactive } = {}) => j(cls.legendItem, inactive && 'is-inactive'),
662
+ legendSwatch: ({ series, shape } = {}) =>
663
+ j(
664
+ cls.legendSwatch,
665
+ series === 1 && cls.legendSwatch1,
666
+ series === 2 && cls.legendSwatch2,
667
+ series === 3 && cls.legendSwatch3,
668
+ series === 4 && cls.legendSwatch4,
669
+ series === 5 && cls.legendSwatch5,
670
+ series === 6 && cls.legendSwatch6,
671
+ series === 7 && cls.legendSwatch7,
672
+ series === 8 && cls.legendSwatch8,
673
+ shape === 'circle' && cls.legendSwatchCircle,
674
+ shape === 'line' && cls.legendSwatchLine,
675
+ ),
676
+ annotation: ({ variant = 'callout', tone = 'accent', motion } = {}) =>
677
+ j(
678
+ cls.annotation,
679
+ variant === 'label' && cls.annotationLabelVariant,
680
+ variant === 'callout' && cls.annotationCallout,
681
+ variant === 'elbow' && cls.annotationElbow,
682
+ variant === 'curve' && cls.annotationCurve,
683
+ variant === 'circle' && cls.annotationCircle,
684
+ variant === 'rect' && cls.annotationRect,
685
+ variant === 'threshold' && cls.annotationThreshold,
686
+ variant === 'badge' && cls.annotationBadgeVariant,
687
+ variant === 'bracket' && cls.annotationBracket,
688
+ variant === 'band' && cls.annotationBand,
689
+ variant === 'slope' && cls.annotationSlope,
690
+ variant === 'compare' && cls.annotationCompare,
691
+ variant === 'cluster' && cls.annotationCluster,
692
+ variant === 'axis' && cls.annotationAxis,
693
+ variant === 'timeline' && cls.annotationTimeline,
694
+ variant === 'evidence' && cls.annotationEvidence,
695
+ tone === 'accent' && cls.annotationAccent,
696
+ tone === 'muted' && cls.annotationMuted,
697
+ tone === 'success' && cls.annotationSuccess,
698
+ tone === 'warning' && cls.annotationWarning,
699
+ tone === 'danger' && cls.annotationDanger,
700
+ tone === 'info' && cls.annotationInfo,
701
+ motion === 'draw' && cls.annotationDraw,
702
+ motion === 'pulse' && cls.annotationPulse,
703
+ motion === 'reveal' && cls.annotationReveal,
704
+ motion === 'focus' && cls.annotationFocus,
705
+ ),
706
+ mark: ({ style, tone, motion } = {}) =>
707
+ j(
708
+ cls.mark,
709
+ style === 'underline' && cls.markUnderline,
710
+ style === 'box' && cls.markBox,
711
+ style === 'strike' && cls.markStrike,
712
+ tone === 'accent' && cls.markAccent,
713
+ tone === 'success' && cls.markSuccess,
714
+ tone === 'warning' && cls.markWarning,
715
+ tone === 'danger' && cls.markDanger,
716
+ tone === 'info' && cls.markInfo,
717
+ tone === 'muted' && cls.markMuted,
718
+ motion === 'draw' && cls.markDraw,
719
+ ),
720
+ bracketNote: ({ tone } = {}) =>
721
+ j(
722
+ cls.bracketNote,
723
+ tone === 'accent' && cls.bracketNoteAccent,
724
+ tone === 'warning' && cls.bracketNoteWarning,
725
+ tone === 'danger' && cls.bracketNoteDanger,
726
+ tone === 'info' && cls.bracketNoteInfo,
727
+ ),
728
+ connector: ({ tone, dashed, motion } = {}) =>
729
+ j(
730
+ cls.connector,
731
+ tone === 'accent' && cls.connectorAccent,
732
+ tone === 'muted' && cls.connectorMuted,
733
+ tone === 'success' && cls.connectorSuccess,
734
+ tone === 'warning' && cls.connectorWarning,
735
+ tone === 'danger' && cls.connectorDanger,
736
+ tone === 'info' && cls.connectorInfo,
737
+ dashed && cls.connectorDashed,
738
+ motion === 'draw' && cls.connectorDraw,
739
+ ),
740
+ spotlight: ({ ring } = {}) => j(cls.spotlight, ring && cls.spotlightRing),
741
+ crosshair: ({ muted } = {}) => j(cls.crosshair, muted && cls.crosshairMuted),
742
+ sel: ({ state } = {}) =>
743
+ j(
744
+ cls.sel,
745
+ state === 'on' && cls.selOn,
746
+ state === 'off' && cls.selOff,
747
+ state === 'maybe' && cls.selMaybe,
748
+ ),
749
+ citation: ({ chip, state } = {}) => j(cls.citation, chip && cls.citationChip, srcTone(state)),
750
+ source: ({ state } = {}) => j(cls.sourceCard, srcTone(state)),
751
+ provenance: ({ state } = {}) => j(cls.provenance, srcTone(state)),
752
+ state: ({ state, busy } = {}) => j(cls.state, stateTone(state), busy && cls.stateBusy),
753
+ originLabel: ({ ai } = {}) => j(cls.originLabel, ai && cls.originLabelAi),
443
754
  };
444
755
 
445
756
  export default ui;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @ponchia/ui/connectors — dependency-free SVG geometry for leader lines.
3
+ * Pure functions: points/rects in, SVG path strings (or coordinates) out.
4
+ */
5
+
6
+ export interface Point {
7
+ x: number;
8
+ y: number;
9
+ }
10
+
11
+ export interface Rect {
12
+ x: number;
13
+ y: number;
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ export type Side = 'top' | 'right' | 'bottom' | 'left' | 'center';
19
+ export type ConnectorShape = 'straight' | 'elbow' | 'curve';
20
+
21
+ export interface ConnectorPathOptions {
22
+ from: Point;
23
+ to: Point;
24
+ shape?: ConnectorShape;
25
+ /** Curve control-point reach along the dominant axis (curve shape). Default 0.5. */
26
+ curvature?: number;
27
+ /** Turn position 0..1 along the span (elbow shape). Default 0.5. */
28
+ mid?: number;
29
+ }
30
+
31
+ export interface ConnectRectsOptions {
32
+ fromRect: Rect;
33
+ toRect: Rect;
34
+ /** Anchor edges. Omit both to auto-pick facing edges from the rects. */
35
+ fromSide?: Side;
36
+ toSide?: Side;
37
+ shape?: ConnectorShape;
38
+ curvature?: number;
39
+ mid?: number;
40
+ }
41
+
42
+ export interface ConnectRectsResult {
43
+ /** SVG path data. */
44
+ d: string;
45
+ from: Point;
46
+ to: Point;
47
+ /**
48
+ * The path's **end-tangent** at `to` in radians (`endTangentAngle(from, to,
49
+ * shape)`) — the direction the path arrives, so rotating an arrowhead at `to`
50
+ * by this points it along the path. Equals the straight `from`→`to` angle for
51
+ * `shape: 'straight'`; axis-aligned for `elbow`/`curve`.
52
+ */
53
+ angle: number;
54
+ }
55
+
56
+ export declare function anchorPoint(rect: Rect, side?: Side): Point;
57
+ export declare function angleBetween(from: Point, to: Point): number;
58
+ export declare function straightPath(from: Point, to: Point): string;
59
+ export declare function elbowPath(from: Point, to: Point, opts?: { mid?: number }): string;
60
+ export declare function curvePath(from: Point, to: Point, opts?: { curvature?: number }): string;
61
+ export declare function connectorPath(opts: ConnectorPathOptions): string;
62
+ export declare function arrowHead(p: Point, angle: number, size?: number): string;
63
+ export declare function dotMark(p: Point, radius?: number): string;
64
+ export declare function autoSides(
65
+ fromRect: Rect,
66
+ toRect: Rect,
67
+ ): { from: Side; to: Side };
68
+ /** Angle (radians) at which a `shape` path arrives at `to` — the chord for
69
+ * `straight`, axis-aligned for `elbow`/`curve`. Use it to rotate an end marker. */
70
+ export declare function endTangentAngle(from: Point, to: Point, shape?: ConnectorShape): number;
71
+ export declare function connectRects(opts: ConnectRectsOptions): ConnectRectsResult;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @ponchia/ui/connectors — dependency-free SVG geometry for connecting two
3
+ * elements (or two points) with a leader line.
4
+ *
5
+ * Pure functions only: they take points/rects and return SVG path strings (or
6
+ * resolved coordinates). They own no DOM, no scales, and no live tracking —
7
+ * that optional glue lives in `@ponchia/ui/behaviors` (`initConnectors`). This
8
+ * is the page-coordinate, element-to-element cousin of the figure-coordinate
9
+ * `@ponchia/ui/annotations` helpers.
10
+ *
11
+ * import { connectRects } from '@ponchia/ui/connectors';
12
+ * const { d } = connectRects({ fromRect: a, toRect: b, shape: 'elbow' });
13
+ */
14
+
15
+ const PRECISION = 1000;
16
+
17
+ function finite(name, value, fallback) {
18
+ const v = value ?? fallback;
19
+ if (!Number.isFinite(v)) throw new TypeError(`${name} must be a finite number`);
20
+ return v;
21
+ }
22
+
23
+ function dimension(name, value, fallback) {
24
+ const v = finite(name, value, fallback);
25
+ if (v < 0) throw new RangeError(`${name} must be greater than or equal to 0`);
26
+ return v;
27
+ }
28
+
29
+ function fmt(value) {
30
+ const rounded = Math.round((Object.is(value, -0) ? 0 : value) * PRECISION) / PRECISION;
31
+ return String(Object.is(rounded, -0) ? 0 : rounded);
32
+ }
33
+
34
+ function point(x, y) {
35
+ return `${fmt(x)},${fmt(y)}`;
36
+ }
37
+
38
+ function clamp(value, min, max) {
39
+ return Math.min(max, Math.max(min, value));
40
+ }
41
+
42
+ /** A point on a rect's edge (or centre). `rect` is `{ x, y, width, height }`. */
43
+ export function anchorPoint(rect, side = 'center') {
44
+ const x = finite('rect.x', rect?.x, 0);
45
+ const y = finite('rect.y', rect?.y, 0);
46
+ const w = dimension('rect.width', rect?.width, 0);
47
+ const h = dimension('rect.height', rect?.height, 0);
48
+ switch (side) {
49
+ case 'top':
50
+ return { x: x + w / 2, y };
51
+ case 'bottom':
52
+ return { x: x + w / 2, y: y + h };
53
+ case 'left':
54
+ return { x, y: y + h / 2 };
55
+ case 'right':
56
+ return { x: x + w, y: y + h / 2 };
57
+ case 'center':
58
+ default:
59
+ return { x: x + w / 2, y: y + h / 2 };
60
+ }
61
+ }
62
+
63
+ /** Angle (radians) from `from` to `to`. */
64
+ export function angleBetween(from, to) {
65
+ return Math.atan2(
66
+ finite('to.y', to?.y) - finite('from.y', from?.y),
67
+ finite('to.x', to?.x) - finite('from.x', from?.x),
68
+ );
69
+ }
70
+
71
+ export function straightPath(from, to) {
72
+ return `M${point(finite('from.x', from?.x), finite('from.y', from?.y))}L${point(
73
+ finite('to.x', to?.x),
74
+ finite('to.y', to?.y),
75
+ )}`;
76
+ }
77
+
78
+ /** Right-angle dogleg. Turns on the dominant axis at `mid` (0..1) of the span. */
79
+ export function elbowPath(from, to, opts = {}) {
80
+ const fx = finite('from.x', from?.x);
81
+ const fy = finite('from.y', from?.y);
82
+ const tx = finite('to.x', to?.x);
83
+ const ty = finite('to.y', to?.y);
84
+ const mid = clamp(finite('mid', opts.mid, 0.5), 0, 1);
85
+ const dx = tx - fx;
86
+ const dy = ty - fy;
87
+ if (Math.abs(dx) >= Math.abs(dy)) {
88
+ const mx = fx + dx * mid;
89
+ return `M${point(fx, fy)}H${fmt(mx)}V${fmt(ty)}H${fmt(tx)}`;
90
+ }
91
+ const my = fy + dy * mid;
92
+ return `M${point(fx, fy)}V${fmt(my)}H${fmt(tx)}V${fmt(ty)}`;
93
+ }
94
+
95
+ /** Cubic curve; control points extend along the dominant axis by `curvature`. */
96
+ export function curvePath(from, to, opts = {}) {
97
+ const fx = finite('from.x', from?.x);
98
+ const fy = finite('from.y', from?.y);
99
+ const tx = finite('to.x', to?.x);
100
+ const ty = finite('to.y', to?.y);
101
+ const k = finite('curvature', opts.curvature, 0.5);
102
+ const dx = tx - fx;
103
+ const dy = ty - fy;
104
+ const horizontal = Math.abs(dx) >= Math.abs(dy);
105
+ const c1 = horizontal ? { x: fx + dx * k, y: fy } : { x: fx, y: fy + dy * k };
106
+ const c2 = horizontal ? { x: tx - dx * k, y: ty } : { x: tx, y: ty - dy * k };
107
+ return `M${point(fx, fy)}C${point(c1.x, c1.y)} ${point(c2.x, c2.y)} ${point(tx, ty)}`;
108
+ }
109
+
110
+ /** Build a path between two points by `shape` (`straight` | `elbow` | `curve`). */
111
+ export function connectorPath(opts = {}) {
112
+ const { from, to, shape = 'straight' } = opts;
113
+ if (shape === 'elbow') return elbowPath(from, to, opts);
114
+ if (shape === 'curve') return curvePath(from, to, opts);
115
+ return straightPath(from, to);
116
+ }
117
+
118
+ /** A filled triangle arrowhead at `p`, pointing along `angle` (radians). */
119
+ export function arrowHead(p, angle, size = 8) {
120
+ const px = finite('p.x', p?.x);
121
+ const py = finite('p.y', p?.y);
122
+ const a = finite('angle', angle, 0);
123
+ const s = dimension('size', size, 8);
124
+ const back = a + Math.PI;
125
+ const spread = 0.45;
126
+ const p1 = { x: px + Math.cos(back - spread) * s, y: py + Math.sin(back - spread) * s };
127
+ const p2 = { x: px + Math.cos(back + spread) * s, y: py + Math.sin(back + spread) * s };
128
+ return `M${point(px, py)}L${point(p1.x, p1.y)}L${point(p2.x, p2.y)}Z`;
129
+ }
130
+
131
+ /** A filled dot at `p`. */
132
+ export function dotMark(p, radius = 3) {
133
+ const px = finite('p.x', p?.x);
134
+ const py = finite('p.y', p?.y);
135
+ const r = dimension('radius', radius, 3);
136
+ if (r === 0) return '';
137
+ return `M${point(px, py - r)}A${fmt(r)},${fmt(r)} 0 1 1 ${point(px, py + r)}A${fmt(r)},${fmt(
138
+ r,
139
+ )} 0 1 1 ${point(px, py - r)}Z`;
140
+ }
141
+
142
+ /** Pick facing edges from the rects' relative centres. */
143
+ export function autoSides(fromRect, toRect) {
144
+ const fc = anchorPoint(fromRect, 'center');
145
+ const tc = anchorPoint(toRect, 'center');
146
+ const dx = tc.x - fc.x;
147
+ const dy = tc.y - fc.y;
148
+ if (Math.abs(dx) >= Math.abs(dy)) {
149
+ return dx >= 0 ? { from: 'right', to: 'left' } : { from: 'left', to: 'right' };
150
+ }
151
+ return dy >= 0 ? { from: 'bottom', to: 'top' } : { from: 'top', to: 'bottom' };
152
+ }
153
+
154
+ /**
155
+ * Connect two rects. Resolves anchor points (explicit `fromSide`/`toSide`, else
156
+ * auto), builds the path, and returns `{ d, from, to, angle }` so the caller can
157
+ * place an arrowhead/dot at `to` rotated by `angle`.
158
+ */
159
+ /** Angle (radians) at which a `shape` path *arrives* at `to` — straight is the
160
+ * chord; elbow/curve arrive axis-aligned along the dominant axis. Rotate an
161
+ * end marker by this so it points along the path, not the chord. */
162
+ export function endTangentAngle(from, to, shape = 'straight') {
163
+ if (shape === 'straight') return angleBetween(from, to);
164
+ const dx = finite('to.x', to?.x) - finite('from.x', from?.x);
165
+ const dy = finite('to.y', to?.y) - finite('from.y', from?.y);
166
+ if (Math.abs(dx) >= Math.abs(dy)) return dx >= 0 ? 0 : Math.PI;
167
+ return dy >= 0 ? Math.PI / 2 : -Math.PI / 2;
168
+ }
169
+
170
+ export function connectRects(opts = {}) {
171
+ const { fromRect, toRect, shape = 'straight', curvature, mid } = opts;
172
+ // Honor each side override independently; auto-pick whichever is unset.
173
+ const auto = autoSides(fromRect, toRect);
174
+ const sides = { from: opts.fromSide || auto.from, to: opts.toSide || auto.to };
175
+ const from = anchorPoint(fromRect, sides.from);
176
+ const to = anchorPoint(toRect, sides.to);
177
+ const d = connectorPath({ from, to, shape, curvature, mid });
178
+ return { d, from, to, angle: endTangentAngle(from, to, shape) };
179
+ }
@@ -0,0 +1,21 @@
1
+ /* ==========================================================================
2
+ analytical — convenience roll-up of the opt-in analytical-primitive layers.
3
+
4
+ Import this one file instead of the seven leaves when you're building
5
+ analytical / generated-report UI:
6
+
7
+ @import '@ponchia/ui';
8
+ @import '@ponchia/ui/css/analytical.css';
9
+
10
+ Add @ponchia/ui/css/dataviz.css for the chart colour palette (legend swatches
11
+ fall back to the accent without it) and @ponchia/ui/css/report.css for the
12
+ document grammar as needed — those are separate concerns, kept opt-in.
13
+ ========================================================================== */
14
+
15
+ @import url('./annotations.css') layer(bronto);
16
+ @import url('./legend.css') layer(bronto);
17
+ @import url('./marks.css') layer(bronto);
18
+ @import url('./connectors.css') layer(bronto);
19
+ @import url('./spotlight.css') layer(bronto);
20
+ @import url('./crosshair.css') layer(bronto);
21
+ @import url('./selection.css') layer(bronto);