@artooi/ag-ui-web-component 0.32.0 → 0.33.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/src/ui/styles.ts CHANGED
@@ -45,6 +45,30 @@ export const STYLES = `
45
45
  --_text: var(--ag-ui-text, var(--_fg));
46
46
  --_surface: var(--ag-ui-surface, transparent);
47
47
 
48
+ /* Message action row: the control box and the mark inside it. The box has a
49
+ floor of 24px so it stays a reliable target at every density. */
50
+ --_action-size: var(--ag-ui-action-size, 28px);
51
+ --_action-icon-size: var(--ag-ui-action-icon-size, 15px);
52
+ --_tooltip-bg: var(--ag-ui-tooltip-bg, #1f2430);
53
+ --_tooltip-fg: var(--ag-ui-tooltip-fg, #f5f6fa);
54
+
55
+ /* Text and marks drawn on top of the accent and danger fills. Separate
56
+ tokens because they are not free: a host that themes the accent to a pale
57
+ colour has white-on-pale with no way to correct it, which is the one
58
+ theming change that makes a control unreadable rather than merely off. */
59
+ --_on-accent: var(--ag-ui-on-accent, #ffffff);
60
+ --_on-danger: var(--ag-ui-on-danger, #ffffff);
61
+
62
+ /* Resize grips: the corner squares and the edge strips between them. Wide
63
+ enough to hit without being wide enough to swallow a click on the content
64
+ underneath, and adjustable for a host with a coarser pointer. */
65
+ --_grip-corner: var(--ag-ui-grip-corner, 14px);
66
+ --_grip-edge: var(--ag-ui-grip-edge, 6px);
67
+ --_grip-edge-docked: var(--ag-ui-grip-edge-docked, 8px);
68
+ /* The mark drawn inside a grip, as opposed to the area it answers to. */
69
+ --_grip-mark-length: var(--ag-ui-grip-mark-length, 28px);
70
+ --_grip-mark-thickness: var(--ag-ui-grip-mark-thickness, 3px);
71
+
48
72
  /* Status accents for tool-call cards. */
49
73
  --_success: var(--ag-ui-success, #15803d);
50
74
  --_danger: var(--ag-ui-danger, #b91c1c);
@@ -55,6 +79,12 @@ export const STYLES = `
55
79
  --_tool-icon-done: var(--ag-ui-tool-icon-done, "✓");
56
80
  --_tool-icon-error: var(--ag-ui-tool-icon-error, "✕");
57
81
  --_tool-icon-declined: var(--ag-ui-tool-icon-declined, "⊘");
82
+
83
+ /* Disclosure marks on every expandable row. Tokenised for the same reason
84
+ the status icons above are: a host re-theming one set and not the other
85
+ ends up with two vocabularies in one transcript. */
86
+ --_disclosure-collapsed: var(--ag-ui-disclosure-collapsed, "▸");
87
+ --_disclosure-expanded: var(--ag-ui-disclosure-expanded, "▾");
58
88
  --_tool-spin-duration: var(--ag-ui-tool-spin-duration, 0.7s);
59
89
 
60
90
  /* Answer well (opt-in via data-answer-well) — boxes a whole assistant turn. */
@@ -335,6 +365,9 @@ export const STYLES = `
335
365
  font: inherit;
336
366
  cursor: pointer;
337
367
  pointer-events: auto;
368
+ /* A touch drag on the launcher moves it; without this the page scrolls under
369
+ the finger instead and the launcher never moves at all. */
370
+ touch-action: none;
338
371
  opacity: 0;
339
372
  transform: scale(0.4);
340
373
  visibility: hidden;
@@ -364,6 +397,15 @@ export const STYLES = `
364
397
  transform: scale(0.94);
365
398
  }
366
399
 
400
+ /* A drag is a press and a hover at the same time, so both scale rules above are
401
+ live throughout it. Cancelling them is what keeps the launcher the size of
402
+ the thing under the pointer while it travels; the position itself comes from
403
+ inset, which nothing here transitions, so it tracks the pointer exactly. */
404
+ :host([collapsed]) .launcher[data-dragging] {
405
+ transform: none;
406
+ cursor: grabbing;
407
+ }
408
+
367
409
  .launcher .icon-holder {
368
410
  width: var(--_launcher-icon-size);
369
411
  height: var(--_launcher-icon-size);
@@ -558,6 +600,31 @@ export const STYLES = `
558
600
  transform-origin: bottom left;
559
601
  }
560
602
 
603
+ /* Once the launcher has been dragged the element places itself, and stamps the
604
+ corner it chose to open away from. The morph has to start at that same
605
+ corner: scaling out of the one the placement originally guessed reads as the
606
+ panel leaping across the screen before it opens.
607
+
608
+ Equal specificity to the placement rule above, so source order is what lets
609
+ the stamped value win -- the same arrangement the resize grip uses, and the
610
+ same trap. Write these with = on the whole hyphenated token; a ~= would match
611
+ whitespace-separated words and so could never match at all. */
612
+ :host([data-expand-corner="top-left"]) .chat {
613
+ transform-origin: top left;
614
+ }
615
+
616
+ :host([data-expand-corner="top-right"]) .chat {
617
+ transform-origin: top right;
618
+ }
619
+
620
+ :host([data-expand-corner="bottom-left"]) .chat {
621
+ transform-origin: bottom left;
622
+ }
623
+
624
+ :host([data-expand-corner="bottom-right"]) .chat {
625
+ transform-origin: bottom right;
626
+ }
627
+
561
628
  /* The two in-flow placements keep the original collapse: hide the body, let
562
629
  the host shrink to the header bar. */
563
630
  :host([collapsed]:is([placement="embedded"], [placement="page"])) {
@@ -983,11 +1050,11 @@ export const STYLES = `
983
1050
  }
984
1051
 
985
1052
  .thoughts-toggle::before {
986
- content: " ";
1053
+ content: var(--_disclosure-expanded) " ";
987
1054
  }
988
1055
 
989
1056
  .thoughts-toggle[aria-expanded="false"]::before {
990
- content: " ";
1057
+ content: var(--_disclosure-collapsed) " ";
991
1058
  }
992
1059
 
993
1060
  /* A gentle pulse on the label while reasoning is still streaming. */
@@ -1329,14 +1396,14 @@ export const STYLES = `
1329
1396
  }
1330
1397
 
1331
1398
  .subagent-row::after {
1332
- content: "▸";
1399
+ content: var(--_disclosure-collapsed);
1333
1400
  flex: none;
1334
1401
  margin-left: auto;
1335
1402
  color: var(--_accent);
1336
1403
  }
1337
1404
 
1338
1405
  .subagent-row[aria-expanded="true"]::after {
1339
- content: "▾";
1406
+ content: var(--_disclosure-expanded);
1340
1407
  }
1341
1408
 
1342
1409
  .subagent-row:disabled::after {
@@ -1463,16 +1530,23 @@ export const STYLES = `
1463
1530
  }
1464
1531
 
1465
1532
  .tool-call-toggle::before {
1466
- content: " ";
1533
+ content: var(--_disclosure-collapsed) " ";
1467
1534
  }
1468
1535
 
1469
1536
  .tool-call-toggle[aria-expanded="true"]::before {
1470
- content: " ";
1537
+ content: var(--_disclosure-expanded) " ";
1471
1538
  }
1472
1539
 
1473
- /* Resize handle: a corner grip on a floating panel, an edge grip on a docked
1474
- one. Absent entirely where the placement is full-bleed, since there is
1475
- nothing to drag. */
1540
+ /* Resize grips: every edge and every corner, so the panel can be dragged from
1541
+ whichever side the reader is already near. Absent entirely where the
1542
+ placement is full-bleed, since there is nothing to drag.
1543
+
1544
+ The whole set is always laid out. Which edges the *layout* pins is not a
1545
+ question the stylesheet needs to answer any more -- it decided where the one
1546
+ grip went, and there is no longer one grip. The element still measures and
1547
+ stamps data-resize-anchor, because it decides what a drag on a pinned edge
1548
+ costs in position and which grip carries the tab stop, but no rule here
1549
+ reads it. */
1476
1550
  .resize-handle {
1477
1551
  position: absolute;
1478
1552
  z-index: 2;
@@ -1485,95 +1559,102 @@ export const STYLES = `
1485
1559
  outline-offset: -2px;
1486
1560
  }
1487
1561
 
1488
- /* The grip sits on the corner opposite the panel's anchor, because a resize
1489
- measures from whichever edge is not moving.
1562
+ /* Corners first in the file and last in the DOM, so they take the pointer
1563
+ where they overlap an edge strip. */
1564
+ .resize-handle--top-left,
1565
+ .resize-handle--top-right,
1566
+ .resize-handle--bottom-left,
1567
+ .resize-handle--bottom-right {
1568
+ width: var(--_grip-corner);
1569
+ height: var(--_grip-corner);
1570
+ }
1490
1571
 
1491
- Default is floating: pinned bottom-right, so the grip is top-left. */
1492
- .resize-handle {
1572
+ .resize-handle--top-left {
1493
1573
  top: 0;
1494
1574
  left: 0;
1495
- width: 14px;
1496
- height: 14px;
1497
1575
  cursor: nwse-resize;
1498
1576
  }
1499
1577
 
1500
- /* Placement is only the opening guess, and these two rules are all it is good
1501
- for. Embedded sits in normal flow, pinned top-left, so it grows bottom-right;
1502
- bottom-left is pinned there, so its free corner is top-right. */
1503
- :host([placement="embedded"]) .resize-handle {
1504
- top: auto;
1505
- left: auto;
1578
+ .resize-handle--top-right {
1579
+ top: 0;
1506
1580
  right: 0;
1507
- bottom: 0;
1508
- cursor: nwse-resize;
1581
+ cursor: nesw-resize;
1509
1582
  }
1510
1583
 
1511
- :host([placement="bottom-left"]) .resize-handle {
1512
- left: auto;
1513
- right: 0;
1584
+ .resize-handle--bottom-left {
1585
+ bottom: 0;
1586
+ left: 0;
1514
1587
  cursor: nesw-resize;
1515
1588
  }
1516
1589
 
1517
- /* Then the measurement corrects it. Which edges are held still belongs to the
1518
- host's layout, not to placement -- a floating panel a host right-aligns is
1519
- anchored bottom-left -- so the element measures them and stamps a single
1520
- hyphenated "<y>-<x>" token here. Equal specificity to the rules above, so
1521
- source order is what lets the measured value win.
1522
-
1523
- Two traps, both of which silently draw the grip on the corner that moves:
1524
-
1525
- Never write these as [data-resize-anchor~="left"]. The stamped value is one
1526
- hyphenated token and ~= matches whitespace-separated words, so it can never
1527
- match.
1528
-
1529
- Each rule must set both sides of its axis, not only the side it moves, or it
1530
- cannot undo a placement guess that flipped the other way. */
1531
- :host([data-resize-anchor$="-left"]) .resize-handle {
1532
- left: auto;
1590
+ .resize-handle--bottom-right {
1591
+ bottom: 0;
1533
1592
  right: 0;
1593
+ cursor: nwse-resize;
1594
+ }
1595
+
1596
+ /* Edge strips, held clear of the corners at both ends so a corner drag is
1597
+ never swallowed by the edge next to it. */
1598
+ .resize-handle--left,
1599
+ .resize-handle--right {
1600
+ top: var(--_grip-corner);
1601
+ bottom: var(--_grip-corner);
1602
+ width: var(--_grip-edge);
1603
+ cursor: ew-resize;
1534
1604
  }
1535
1605
 
1536
- :host([data-resize-anchor$="-right"]) .resize-handle {
1606
+ .resize-handle--left {
1537
1607
  left: 0;
1538
- right: auto;
1539
1608
  }
1540
1609
 
1541
- :host([data-resize-anchor^="top-"]) .resize-handle {
1542
- top: auto;
1543
- bottom: 0;
1610
+ .resize-handle--right {
1611
+ right: 0;
1544
1612
  }
1545
1613
 
1546
- :host([data-resize-anchor^="bottom-"]) .resize-handle {
1547
- top: 0;
1548
- bottom: auto;
1614
+ .resize-handle--top,
1615
+ .resize-handle--bottom {
1616
+ left: var(--_grip-corner);
1617
+ right: var(--_grip-corner);
1618
+ height: var(--_grip-edge);
1619
+ cursor: ns-resize;
1549
1620
  }
1550
1621
 
1551
- /* One axis flipped means the drag runs along the other diagonal. */
1552
- :host([data-resize-anchor="top-left"]) .resize-handle,
1553
- :host([data-resize-anchor="bottom-right"]) .resize-handle {
1554
- cursor: nwse-resize;
1622
+ .resize-handle--top {
1623
+ top: 0;
1555
1624
  }
1556
1625
 
1557
- :host([data-resize-anchor="top-right"]) .resize-handle,
1558
- :host([data-resize-anchor="bottom-left"]) .resize-handle {
1559
- cursor: nesw-resize;
1626
+ .resize-handle--bottom {
1627
+ bottom: 0;
1560
1628
  }
1561
1629
 
1562
- /* Docked: the placement owns the height, so only the inner edge is the user's --
1563
- an edge, not a corner, so this outranks the measured anchor by coming after. */
1564
- :host([placement="sidebar"]) .resize-handle,
1565
- :host([placement="side"]) .resize-handle {
1566
- top: 0;
1567
- bottom: auto;
1568
- width: 8px;
1569
- height: 100%;
1570
- cursor: ew-resize;
1630
+ /* Docked: the placement owns the height, so the horizontal edges and every
1631
+ corner are inert and must not advertise a drag that does nothing. The two
1632
+ vertical edges remain, which is the same affordance these placements had
1633
+ when there was one grip -- now on both sides, since either may be the inner
1634
+ one depending on which side the rail is docked to. */
1635
+ :host([placement="sidebar"]) .resize-handle--top,
1636
+ :host([placement="sidebar"]) .resize-handle--bottom,
1637
+ :host([placement="sidebar"]) .resize-handle--top-left,
1638
+ :host([placement="sidebar"]) .resize-handle--top-right,
1639
+ :host([placement="sidebar"]) .resize-handle--bottom-left,
1640
+ :host([placement="sidebar"]) .resize-handle--bottom-right,
1641
+ :host([placement="side"]) .resize-handle--top,
1642
+ :host([placement="side"]) .resize-handle--bottom,
1643
+ :host([placement="side"]) .resize-handle--top-left,
1644
+ :host([placement="side"]) .resize-handle--top-right,
1645
+ :host([placement="side"]) .resize-handle--bottom-left,
1646
+ :host([placement="side"]) .resize-handle--bottom-right {
1647
+ display: none;
1571
1648
  }
1572
1649
 
1573
- /* Docked to the left, so the inner edge is the right-hand one. */
1574
- :host([placement="sidebar"][data-side="left"]) .resize-handle {
1575
- left: auto;
1576
- right: 0;
1650
+ /* The vertical edges run the full height once no corner shares them. */
1651
+ :host([placement="sidebar"]) .resize-handle--left,
1652
+ :host([placement="sidebar"]) .resize-handle--right,
1653
+ :host([placement="side"]) .resize-handle--left,
1654
+ :host([placement="side"]) .resize-handle--right {
1655
+ top: 0;
1656
+ bottom: 0;
1657
+ width: var(--_grip-edge-docked);
1577
1658
  }
1578
1659
 
1579
1660
  /* Full-bleed: nothing to drag. */
@@ -1582,9 +1663,62 @@ export const STYLES = `
1582
1663
  display: none;
1583
1664
  }
1584
1665
 
1585
- .resize-handle[data-dragging] {
1666
+ /* The visible mark on a grip.
1667
+
1668
+ Filling the whole strip was what the single corner grip did, and at 14px
1669
+ square nobody ever saw it. On a strip running the length of an edge the same
1670
+ fill reads as a border the panel grew -- square ended, stopping short of the
1671
+ corner radius at both ends, and easily mistaken for a rendering fault rather
1672
+ than for something to grab.
1673
+
1674
+ So the hit area stays the full strip and the mark is a short pill centred on
1675
+ it, which cannot be read as an edge of anything and never meets the radius.
1676
+
1677
+ It shows on hover and focus as well as during the drag. With eight grips, a
1678
+ mark that appears only once you are already dragging is a mark that never
1679
+ told anyone the grips were there. */
1680
+ .resize-handle::after {
1681
+ content: "";
1682
+ position: absolute;
1683
+ top: 50%;
1684
+ left: 50%;
1685
+ transform: translate(-50%, -50%);
1686
+ border-radius: 999px;
1586
1687
  background: var(--_accent);
1587
- opacity: 0.35;
1688
+ opacity: 0;
1689
+ transition: opacity var(--_motion) var(--_ease);
1690
+ }
1691
+
1692
+ .resize-handle--top::after,
1693
+ .resize-handle--bottom::after {
1694
+ width: var(--_grip-mark-length);
1695
+ height: var(--_grip-mark-thickness);
1696
+ }
1697
+
1698
+ .resize-handle--left::after,
1699
+ .resize-handle--right::after {
1700
+ width: var(--_grip-mark-thickness);
1701
+ height: var(--_grip-mark-length);
1702
+ }
1703
+
1704
+ /* A corner has no length to run along, so it gets a dot instead of a pill. */
1705
+ .resize-handle--top-left::after,
1706
+ .resize-handle--top-right::after,
1707
+ .resize-handle--bottom-left::after,
1708
+ .resize-handle--bottom-right::after {
1709
+ width: var(--_grip-mark-thickness);
1710
+ height: var(--_grip-mark-thickness);
1711
+ }
1712
+
1713
+ .resize-handle:hover::after,
1714
+ .resize-handle:focus-visible::after {
1715
+ opacity: 0.5;
1716
+ }
1717
+
1718
+ /* Equal specificity to the pair above, so source order is what makes the drag
1719
+ the stronger of the two states. */
1720
+ .resize-handle[data-dragging]::after {
1721
+ opacity: 0.9;
1588
1722
  }
1589
1723
 
1590
1724
  /* ── Composer ───────────────────────────────────────────────────────────────
@@ -1698,7 +1832,7 @@ export const STYLES = `
1698
1832
  border: none;
1699
1833
  border-radius: 50%;
1700
1834
  background: var(--_accent);
1701
- color: #ffffff;
1835
+ color: var(--_on-accent);
1702
1836
  font: inherit;
1703
1837
  line-height: 1;
1704
1838
  cursor: pointer;
@@ -1772,13 +1906,13 @@ export const STYLES = `
1772
1906
  /* Recording: a red tint + a gentle pulse so it's clearly "live". */
1773
1907
  .voice-btn[data-state="recording"] {
1774
1908
  background: var(--_danger);
1775
- color: #ffffff;
1909
+ color: var(--_on-danger);
1776
1910
  animation: ag-ui-voice-pulse 1.2s ease-in-out infinite;
1777
1911
  }
1778
1912
 
1779
1913
  .voice-btn[data-state="recording"]:hover {
1780
1914
  background: var(--_danger);
1781
- color: #ffffff;
1915
+ color: var(--_on-danger);
1782
1916
  }
1783
1917
 
1784
1918
  @keyframes ag-ui-voice-pulse {
@@ -2068,7 +2202,7 @@ export const STYLES = `
2068
2202
  .confirm-btn--confirm {
2069
2203
  border-color: var(--_accent);
2070
2204
  background: var(--_accent);
2071
- color: #ffffff;
2205
+ color: var(--_on-accent);
2072
2206
  }
2073
2207
 
2074
2208
  /* The session waiver. Deliberately the quietest of the three: it is the widest
@@ -2136,16 +2270,29 @@ export const STYLES = `
2136
2270
  margin-top: -6px;
2137
2271
  }
2138
2272
 
2273
+ /* The row's controls are icon-only, so the box is the whole target. Sized from
2274
+ a variable a host can raise, with a floor rather than a fixed height: the
2275
+ compact density shrinks the font, and a target that shrinks with it lands
2276
+ under the 24px minimum that makes a control reliably tappable. It used to,
2277
+ at roughly 20px square. */
2139
2278
  .message-action {
2279
+ display: inline-flex;
2280
+ align-items: center;
2281
+ justify-content: center;
2282
+ box-sizing: border-box;
2283
+ min-width: var(--_action-size);
2284
+ min-height: var(--_action-size);
2285
+ padding: 4px;
2140
2286
  border: none;
2141
2287
  border-radius: 6px;
2142
- padding: 2px 6px;
2143
2288
  font: inherit;
2144
2289
  line-height: 1.2;
2145
2290
  cursor: pointer;
2146
2291
  background: transparent;
2147
2292
  color: var(--_muted);
2148
2293
  opacity: 0.75;
2294
+ /* The tooltip below is positioned against this box. */
2295
+ position: relative;
2149
2296
  }
2150
2297
 
2151
2298
  .message-action:hover,
@@ -2154,6 +2301,61 @@ export const STYLES = `
2154
2301
  background: var(--_border);
2155
2302
  }
2156
2303
 
2304
+ .message-action-icon {
2305
+ display: inline-flex;
2306
+ width: var(--_action-icon-size);
2307
+ height: var(--_action-icon-size);
2308
+ }
2309
+
2310
+ .message-action-icon .glyph {
2311
+ width: 100%;
2312
+ height: 100%;
2313
+ }
2314
+
2315
+ /* The label, drawn rather than left to the browser.
2316
+
2317
+ A title attribute is the usual answer and covers only half the readers: it
2318
+ never appears on keyboard focus, so tabbing onto an icon-only control shows
2319
+ nothing at all. The attribute stays for the pointer users who expect it, and
2320
+ this shows the same words on hover and on focus alike.
2321
+
2322
+ Left-aligned rather than centred because the row sits at the left edge of an
2323
+ answer inside a scrolling column, and a centred tooltip on the first control
2324
+ is clipped by that column. Growing rightward keeps it inside.
2325
+
2326
+ Not exposed to assistive technology: the button already carries the same
2327
+ string as its accessible name, and this would be a second copy of it. */
2328
+ .message-action::after {
2329
+ content: attr(data-tooltip);
2330
+ position: absolute;
2331
+ bottom: calc(100% + 4px);
2332
+ left: 0;
2333
+ z-index: 3;
2334
+ padding: 3px 6px;
2335
+ border-radius: 4px;
2336
+ background: var(--_tooltip-bg);
2337
+ color: var(--_tooltip-fg);
2338
+ font-size: 11px;
2339
+ line-height: 1.4;
2340
+ white-space: nowrap;
2341
+ pointer-events: none;
2342
+ opacity: 0;
2343
+ transition: opacity var(--_motion) var(--_ease);
2344
+ }
2345
+
2346
+ .message-action:hover::after,
2347
+ .message-action:focus-visible::after {
2348
+ opacity: 1;
2349
+ }
2350
+
2351
+ /* Touch has no hover to reveal it, and a tooltip that latches open under a
2352
+ finger covers the answer it belongs to. */
2353
+ @media (hover: none) {
2354
+ .message-action::after {
2355
+ content: none;
2356
+ }
2357
+ }
2358
+
2157
2359
  .message-action[aria-pressed="true"] {
2158
2360
  opacity: 1;
2159
2361
  color: var(--_accent);
@@ -2211,7 +2413,7 @@ export const STYLES = `
2211
2413
  .approval-btn--approve {
2212
2414
  border-color: var(--_accent);
2213
2415
  background: var(--_accent);
2214
- color: #ffffff;
2416
+ color: var(--_on-accent);
2215
2417
  }
2216
2418
 
2217
2419
  /* Question card — the built-in ask_user prompt (radios and/or free text). */
@@ -2277,7 +2479,7 @@ export const STYLES = `
2277
2479
  font-weight: 600;
2278
2480
  cursor: pointer;
2279
2481
  background: var(--_accent);
2280
- color: #ffffff;
2482
+ color: var(--_on-accent);
2281
2483
  }
2282
2484
 
2283
2485
  .question-btn:disabled {
@@ -2546,7 +2748,7 @@ export const STYLES = `
2546
2748
  font-weight: 600;
2547
2749
  border-color: var(--_accent);
2548
2750
  background: var(--_accent);
2549
- color: #ffffff;
2751
+ color: var(--_on-accent);
2550
2752
  }
2551
2753
 
2552
2754
  .checkpoint-fork:hover {
@@ -2721,7 +2923,7 @@ export const STYLES = `
2721
2923
  border: none;
2722
2924
  border-radius: 6px;
2723
2925
  background: var(--_danger);
2724
- color: #ffffff;
2926
+ color: var(--_on-danger);
2725
2927
  padding: 3px 10px;
2726
2928
  font: inherit;
2727
2929
  cursor: pointer;
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const VERSION: string = "0.32.0";
1
+ export const VERSION: string = "0.33.0";