@birdycom/omniaui 0.11.2 → 0.12.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.
@@ -0,0 +1,6 @@
1
+ // File: packages/ui/src/carousel.d.ts
2
+ // Types for the looping carousel script (.oc-carousel--loop prev/next buttons).
3
+
4
+ export function loopTarget(position: number, max: number, step: number, direction: 1 | -1): number;
5
+ export function bindCarousel(root: HTMLElement): void;
6
+ export function initCarousels(scope?: ParentNode): void;
@@ -0,0 +1,46 @@
1
+ // File: packages/ui/src/carousel.js
2
+ // Map: next scroll position (wraps at the ends) -> bind one .oc-carousel--loop (prev/next buttons) -> boot
3
+ // Loop for the CSS carousel: the browser's ::scroll-button arrows stop at the first and last slide, so a looping
4
+ // carousel has two real buttons (.oc-carousel-prev / .oc-carousel-next) and this script moves it one slide, from the
5
+ // last slide back to the first and from the first to the last. Swipe, snap, dots and keyboard stay CSS.
6
+
7
+ /**
8
+ * Where to scroll for one step. `position` and `max` are distances from the start edge (positive, also in RTL),
9
+ * `step` is one slide plus the gap. At an end (within 2px, scroll snapping rounds) it wraps around.
10
+ */
11
+ export function loopTarget(position, max, step, direction) {
12
+ if (direction > 0) return position >= max - 2 ? 0 : Math.min(max, position + step);
13
+ return position <= 2 ? max : Math.max(0, position - step);
14
+ }
15
+
16
+ /** Wire the prev/next buttons of one looping carousel (once). */
17
+ export function bindCarousel(root) {
18
+ if (root.hasAttribute("data-oc-loop")) return;
19
+ const viewport = root.querySelector(".oc-carousel-viewport");
20
+ const items = viewport?.querySelectorAll(".oc-carousel-item");
21
+ if (!viewport || !items?.length) return;
22
+ const rtl = getComputedStyle(viewport).direction === "rtl";
23
+ const move = (direction) => {
24
+ const max = viewport.scrollWidth - viewport.clientWidth;
25
+ // One slide plus the gap: the distance between the first two slides (one slide wide when there is only one).
26
+ const step = items.length > 1 ? Math.abs(items[1].offsetLeft - items[0].offsetLeft) : viewport.clientWidth;
27
+ const position = Math.abs(viewport.scrollLeft);
28
+ const target = loopTarget(position, max, step, direction);
29
+ viewport.scrollTo({ left: rtl ? -target : target });
30
+ };
31
+ root.querySelector(".oc-carousel-prev")?.addEventListener("click", () => move(-1));
32
+ root.querySelector(".oc-carousel-next")?.addEventListener("click", () => move(1));
33
+ root.setAttribute("data-oc-loop", "");
34
+ }
35
+
36
+ /** Wire every looping carousel under scope that is not wired yet. */
37
+ export function initCarousels(scope = document) {
38
+ scope.querySelectorAll(".oc-carousel--loop").forEach((root) => bindCarousel(root));
39
+ }
40
+
41
+ // The docs site loads this file as /js/oc-carousel.js. A wrapper import must not scan the page.
42
+ if (typeof document !== "undefined" && String(import.meta.url).includes("oc-carousel")) {
43
+ const boot = () => initCarousels(document);
44
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
45
+ else boot();
46
+ }
@@ -503,6 +503,45 @@ a.oc-item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px
503
503
  .oc-carousel-viewport::scroll-button(inline-end):disabled { opacity: 0.35; cursor: default; }
504
504
  }
505
505
 
506
+ /* Loop (.oc-carousel--loop): after the last slide comes the first again. CSS scroll buttons stop at the ends, so this
507
+ variant has two real buttons (.oc-carousel-prev / .oc-carousel-next) driven by carousel.js; swipe, snap, dots and
508
+ keyboard stay CSS. Until the script has run (data-oc-loop), the buttons stay hidden and the CSS arrows remain. */
509
+ .oc-carousel-prev,
510
+ .oc-carousel-next {
511
+ position: absolute;
512
+ inset-block-start: 50%;
513
+ translate: 0 -50%;
514
+ z-index: 1;
515
+ inline-size: 2.25rem;
516
+ block-size: 2.25rem;
517
+ display: grid;
518
+ place-items: center;
519
+ padding: 0;
520
+ font-size: 1rem;
521
+ color: var(--oc-foreground);
522
+ background: var(--oc-background);
523
+ border: 1px solid var(--oc-border);
524
+ border-radius: 50%;
525
+ box-shadow: 0 2px 8px oklch(from var(--color-black-60) l c h / 0.18);
526
+ cursor: pointer;
527
+ }
528
+ .oc-carousel-prev { inset-inline-start: 0.5rem; }
529
+ .oc-carousel-next { inset-inline-end: 0.5rem; }
530
+ .oc-carousel-prev:hover,
531
+ .oc-carousel-next:hover { background: var(--oc-accent); }
532
+ .oc-carousel-prev:focus-visible,
533
+ .oc-carousel-next:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
534
+ .oc-carousel--loop:not([data-oc-loop]) :is(.oc-carousel-prev, .oc-carousel-next) { display: none; }
535
+ /* Centered on the viewport, not on the carousel with its dots underneath. */
536
+ @supports (anchor-name: --a) {
537
+ .oc-carousel-prev,
538
+ .oc-carousel-next { position-anchor: --oc-carousel; inset-block-start: anchor(center); }
539
+ }
540
+ @supports selector(::scroll-button(inline-start)) {
541
+ .oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-start),
542
+ .oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-end) { content: none; }
543
+ }
544
+
506
545
  /* ===================================================================
507
546
  RESIZABLE — split panes using the native CSS `resize` property (drag the grip in the corner of the resizable panel).
508
547
  A full-length drag handle between panes with keyboard support needs script: wrapper responsibility.
@@ -1,4 +1,4 @@
1
- /* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.11.2. Do not edit. */
1
+ /* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.12.0. Do not edit. */
2
2
  /* Semantic theme tokens: every color is light-dark() of an Omnia palette token (oc-colors.css). */
3
3
  /* Same source as the desktop export: npx omniacss tokens --format json|dtcg|python */
4
4
  :root {
@@ -1363,37 +1363,120 @@ textarea.oc-input {
1363
1363
 
1364
1364
  /* ===================================================================
1365
1365
  TOOLTIP
1366
- Pure CSS via data-tooltip attribute
1366
+ Pure CSS. Text: the data-tooltip attribute (drawn by ::after). Richer content, such as a title above lines
1367
+ of values: a child .oc-tooltip__content. With anchor positioning the bubble is anchored to its element and
1368
+ moves to the side that has room; otherwise it sits on the preferred side (top by default).
1367
1369
  =================================================================== */
1368
1370
 
1369
1371
  .oc-tooltip {
1372
+ --oc-tooltip-bg: oklch(from var(--oc-popover) l c h / 0.94);
1373
+ --oc-tooltip-gap: 8px;
1370
1374
  position: relative;
1371
1375
  cursor: help;
1372
1376
  }
1373
1377
 
1374
- .oc-tooltip::after {
1375
- content: attr(data-tooltip);
1378
+ .oc-tooltip::after,
1379
+ .oc-tooltip > .oc-tooltip__content {
1376
1380
  position: absolute;
1377
- bottom: calc(100% + 6px);
1381
+ bottom: calc(100% + var(--oc-tooltip-gap));
1378
1382
  left: 50%;
1379
- transform: translateX(-50%);
1383
+ translate: -50% 0;
1380
1384
  padding: 6px 10px;
1381
1385
  font-size: 0.75rem;
1382
- line-height: 1.4;
1383
- color: var(--oc-primary-foreground);
1384
- background: var(--oc-primary);
1385
- border-radius: calc(var(--oc-radius) - 2px);
1386
+ font-weight: 500;
1387
+ line-height: 1.45;
1388
+ color: var(--oc-popover-foreground);
1389
+ background: var(--oc-tooltip-bg);
1390
+ border: 1px solid var(--oc-border);
1391
+ border-radius: var(--oc-radius);
1392
+ box-shadow: 0 10px 28px -8px oklch(from var(--color-black-60) l c h / 0.3), 0 2px 6px -2px oklch(from var(--color-black-60) l c h / 0.15);
1393
+ backdrop-filter: blur(12px) saturate(1.4);
1386
1394
  white-space: nowrap;
1387
1395
  pointer-events: none;
1388
1396
  opacity: 0;
1389
- transition: opacity var(--oc-transition-fast);
1397
+ scale: 0.96;
1398
+ transition: opacity var(--oc-transition-fast), scale 0.18s cubic-bezier(0.2, 0.8, 0.2, 1);
1390
1399
  z-index: 50;
1391
1400
  }
1392
1401
 
1402
+ .oc-tooltip::after { content: attr(data-tooltip); }
1403
+ /* Rich content only: no empty ::after bubble. */
1404
+ .oc-tooltip:not([data-tooltip])::after { content: none; }
1405
+
1393
1406
  /* Also on keyboard focus of the element inside (Tab), not only on mouse hover. */
1394
1407
  .oc-tooltip:hover::after,
1395
- .oc-tooltip:focus-within::after {
1408
+ .oc-tooltip:focus-within::after,
1409
+ .oc-tooltip:hover > .oc-tooltip__content,
1410
+ .oc-tooltip:focus-within > .oc-tooltip__content {
1396
1411
  opacity: 1;
1412
+ scale: 1;
1413
+ }
1414
+
1415
+ @media (prefers-reduced-motion: reduce) {
1416
+ .oc-tooltip::after,
1417
+ .oc-tooltip > .oc-tooltip__content { transition: opacity var(--oc-transition-fast); scale: 1; }
1418
+ }
1419
+
1420
+ /* Several lines: line breaks in data-tooltip (or in .oc-tooltip__content) start a new line. */
1421
+ .oc-tooltip--multiline::after,
1422
+ .oc-tooltip > .oc-tooltip__content {
1423
+ white-space: pre-line;
1424
+ inline-size: max-content;
1425
+ max-inline-size: 20rem;
1426
+ padding: 8px 12px;
1427
+ text-align: start;
1428
+ font-variant-numeric: tabular-nums;
1429
+ }
1430
+
1431
+ /* Title line of the rich content: a bit larger, always starting with a capital. */
1432
+ .oc-tooltip__title {
1433
+ display: block;
1434
+ margin-block-end: 2px;
1435
+ font-size: 0.875rem;
1436
+ font-weight: 700;
1437
+ line-height: 1.35;
1438
+ }
1439
+ .oc-tooltip__title::first-letter { text-transform: uppercase; }
1440
+
1441
+ /* Preferred side (default: top). Without anchor positioning these are fixed places next to the element. */
1442
+ .oc-tooltip--bottom::after,
1443
+ .oc-tooltip--bottom > .oc-tooltip__content { bottom: auto; top: calc(100% + var(--oc-tooltip-gap)); }
1444
+ .oc-tooltip--left::after,
1445
+ .oc-tooltip--left > .oc-tooltip__content { bottom: auto; left: auto; top: 50%; right: calc(100% + var(--oc-tooltip-gap)); translate: 0 -50%; }
1446
+ .oc-tooltip--right::after,
1447
+ .oc-tooltip--right > .oc-tooltip__content { bottom: auto; top: 50%; left: calc(100% + var(--oc-tooltip-gap)); translate: 0 -50%; }
1448
+
1449
+ /* Anchor positioning: fixed, so a scroll container (a wide table, a sidebar) does not clip it, and flipped to the
1450
+ opposite side (then the other axis) when the preferred side has no room. ::after uses its element as implicit
1451
+ anchor; .oc-tooltip__content needs a named anchor, scoped to its own .oc-tooltip (anchor-scope). */
1452
+ @supports (position-area: top) {
1453
+ .oc-tooltip::after {
1454
+ position: fixed;
1455
+ inset: auto;
1456
+ translate: none;
1457
+ margin: var(--oc-tooltip-gap);
1458
+ position-area: top;
1459
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
1460
+ }
1461
+ .oc-tooltip--bottom::after { position-area: bottom; }
1462
+ .oc-tooltip--left::after { position-area: left; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
1463
+ .oc-tooltip--right::after { position-area: right; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
1464
+ }
1465
+
1466
+ @supports (position-area: top) and (anchor-scope: all) {
1467
+ .oc-tooltip { anchor-name: --oc-tooltip; anchor-scope: --oc-tooltip; }
1468
+ .oc-tooltip > .oc-tooltip__content {
1469
+ position: fixed;
1470
+ inset: auto;
1471
+ translate: none;
1472
+ margin: var(--oc-tooltip-gap);
1473
+ position-anchor: --oc-tooltip;
1474
+ position-area: top;
1475
+ position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
1476
+ }
1477
+ .oc-tooltip--bottom > .oc-tooltip__content { position-area: bottom; }
1478
+ .oc-tooltip--left > .oc-tooltip__content { position-area: left; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
1479
+ .oc-tooltip--right > .oc-tooltip__content { position-area: right; position-try-fallbacks: flip-inline, flip-block, flip-block flip-inline; }
1397
1480
  }
1398
1481
 
1399
1482
  /* ===================================================================
@@ -3546,7 +3629,9 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3546
3629
  .oc-btn--pill { border-radius: 999px; }
3547
3630
  .oc-btn--gradient {
3548
3631
  border-radius: 999px;
3549
- background: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to));
3632
+ /* border-box: the gradient also runs under the transparent 1px border of .oc-btn; from the padding box it would
3633
+ repeat in that border and show a thin stripe of the other end's color on both sides. */
3634
+ background: linear-gradient(100deg, var(--oc-accent-from), var(--oc-accent-to)) border-box;
3550
3635
  color: var(--color-white-50);
3551
3636
  box-shadow: 0 10px 30px -10px var(--oc-accent-to);
3552
3637
  transition: transform 0.2s var(--oc-ease-out), box-shadow 0.2s;
@@ -3736,16 +3821,21 @@ details.oc-collapsible[open]::details-content { block-size: auto; }
3736
3821
  background: oklch(from var(--oc-accent-from) l c h / 0.14);
3737
3822
  color: light-dark(var(--oc-accent-from), oklch(from var(--oc-accent-from) max(l, 0.72) c h));
3738
3823
  }
3824
+ /* Pulse: the ring starts at --oc-status-dot-pulse-alpha of the accent and grows to --oc-status-dot-pulse-size.
3825
+ --strong starts at the full accent with a wider ring. */
3739
3826
  .oc-status-dot {
3827
+ --oc-status-dot-pulse-alpha: 0.6;
3828
+ --oc-status-dot-pulse-size: 8px;
3740
3829
  inline-size: 0.5rem;
3741
3830
  aspect-ratio: 1;
3742
3831
  border-radius: 50%;
3743
3832
  background: var(--oc-accent-from);
3744
3833
  animation: oc-pulse 2s infinite;
3745
3834
  }
3835
+ .oc-status-dot--strong { --oc-status-dot-pulse-alpha: 1; --oc-status-dot-pulse-size: 10px; }
3746
3836
  @keyframes oc-pulse {
3747
- 0% { box-shadow: 0 0 0 0 oklch(from var(--oc-accent-from) l c h / 0.6); }
3748
- 70%, 100% { box-shadow: 0 0 0 8px transparent; }
3837
+ 0% { box-shadow: 0 0 0 0 oklch(from var(--oc-accent-from) l c h / var(--oc-status-dot-pulse-alpha)); }
3838
+ 70%, 100% { box-shadow: 0 0 0 var(--oc-status-dot-pulse-size) transparent; }
3749
3839
  }
3750
3840
 
3751
3841
  /* Stat strip: big numbers in a bordered row (pairs with .oc-counter for counting up) */
@@ -4680,6 +4770,45 @@ a.oc-item:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px
4680
4770
  .oc-carousel-viewport::scroll-button(inline-end):disabled { opacity: 0.35; cursor: default; }
4681
4771
  }
4682
4772
 
4773
+ /* Loop (.oc-carousel--loop): after the last slide comes the first again. CSS scroll buttons stop at the ends, so this
4774
+ variant has two real buttons (.oc-carousel-prev / .oc-carousel-next) driven by carousel.js; swipe, snap, dots and
4775
+ keyboard stay CSS. Until the script has run (data-oc-loop), the buttons stay hidden and the CSS arrows remain. */
4776
+ .oc-carousel-prev,
4777
+ .oc-carousel-next {
4778
+ position: absolute;
4779
+ inset-block-start: 50%;
4780
+ translate: 0 -50%;
4781
+ z-index: 1;
4782
+ inline-size: 2.25rem;
4783
+ block-size: 2.25rem;
4784
+ display: grid;
4785
+ place-items: center;
4786
+ padding: 0;
4787
+ font-size: 1rem;
4788
+ color: var(--oc-foreground);
4789
+ background: var(--oc-background);
4790
+ border: 1px solid var(--oc-border);
4791
+ border-radius: 50%;
4792
+ box-shadow: 0 2px 8px oklch(from var(--color-black-60) l c h / 0.18);
4793
+ cursor: pointer;
4794
+ }
4795
+ .oc-carousel-prev { inset-inline-start: 0.5rem; }
4796
+ .oc-carousel-next { inset-inline-end: 0.5rem; }
4797
+ .oc-carousel-prev:hover,
4798
+ .oc-carousel-next:hover { background: var(--oc-accent); }
4799
+ .oc-carousel-prev:focus-visible,
4800
+ .oc-carousel-next:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 2px; }
4801
+ .oc-carousel--loop:not([data-oc-loop]) :is(.oc-carousel-prev, .oc-carousel-next) { display: none; }
4802
+ /* Centered on the viewport, not on the carousel with its dots underneath. */
4803
+ @supports (anchor-name: --a) {
4804
+ .oc-carousel-prev,
4805
+ .oc-carousel-next { position-anchor: --oc-carousel; inset-block-start: anchor(center); }
4806
+ }
4807
+ @supports selector(::scroll-button(inline-start)) {
4808
+ .oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-start),
4809
+ .oc-carousel--loop[data-oc-loop] .oc-carousel-viewport::scroll-button(inline-end) { content: none; }
4810
+ }
4811
+
4683
4812
  /* ===================================================================
4684
4813
  RESIZABLE — split panes using the native CSS `resize` property (drag the grip in the corner of the resizable panel).
4685
4814
  A full-length drag handle between panes with keyboard support needs script: wrapper responsibility.
@@ -9720,6 +9849,8 @@ a.oc-message__quote:focus-visible { outline: 2px solid var(--oc-ring); outline-o
9720
9849
  background:
9721
9850
  radial-gradient(circle at 80% 20%, color-mix(in oklch, var(--color-white-50) 30%, transparent), transparent 45%),
9722
9851
  linear-gradient(135deg, var(--oc-accent-from), var(--oc-accent-to));
9852
+ /* The hover zoom grows down and sideways (clipped by the frame), never up over the title bar. */
9853
+ transform-origin: 50% 0;
9723
9854
  transition: scale 0.5s var(--oc-ease-out);
9724
9855
  }
9725
9856