@jay-framework/ui-kit 0.16.4

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,84 @@
1
+ # Accordion
2
+
3
+ Collapsible sections using native `<details>` / `<summary>`. Pure HTML, no component needed.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <div class="accordion">
9
+ <details>
10
+ <summary>Shipping Information</summary>
11
+ <div class="accordion-content">
12
+ <p>Free shipping on orders over $50...</p>
13
+ </div>
14
+ </details>
15
+
16
+ <details>
17
+ <summary>Return Policy</summary>
18
+ <div class="accordion-content">
19
+ <p>30-day return policy...</p>
20
+ </div>
21
+ </details>
22
+
23
+ <details>
24
+ <summary>Size Guide</summary>
25
+ <div class="accordion-content">
26
+ <p>Measurements for each size...</p>
27
+ </div>
28
+ </details>
29
+ </div>
30
+ ```
31
+
32
+ ## CSS
33
+
34
+ ```css
35
+ .accordion details {
36
+ border: 1px solid #ddd;
37
+ border-radius: 4px;
38
+ margin-bottom: 8px;
39
+ }
40
+ summary {
41
+ padding: 12px 16px;
42
+ cursor: pointer;
43
+ font-weight: bold;
44
+ }
45
+ .accordion-content {
46
+ padding: 0 16px 12px;
47
+ }
48
+ summary::marker {
49
+ content: '+ ';
50
+ }
51
+ details[open] summary::marker {
52
+ content: '- ';
53
+ }
54
+ ```
55
+
56
+ ## Single-open accordion
57
+
58
+ Use the `name` attribute — same name means only one section open at a time:
59
+
60
+ ```html
61
+ <details name="faq">
62
+ <summary>Question 1</summary>
63
+ <p>Answer 1</p>
64
+ </details>
65
+ <details name="faq">
66
+ <summary>Question 2</summary>
67
+ <p>Answer 2</p>
68
+ </details>
69
+ ```
70
+
71
+ ## Accessibility & SEO
72
+
73
+ `<details>` / `<summary>` is natively accessible — keyboard (Enter/Space to toggle), screen readers announce expanded/collapsed state. All content is in the DOM — search engines index it regardless of open/closed state.
74
+
75
+ ## Collapsible text (single section)
76
+
77
+ A standalone `<details>` works as a "read more" / "show details" toggle — no wrapper needed:
78
+
79
+ ```html
80
+ <details>
81
+ <summary>Read more</summary>
82
+ <p>Extended content that's hidden by default...</p>
83
+ </details>
84
+ ```
@@ -0,0 +1,40 @@
1
+ # Click Popover
2
+
3
+ Popup dialog on button click with auto-dismiss on click outside. Pure HTML using `popovertarget`, no component needed.
4
+
5
+ For **hover** triggers, use the `popover-menu` headless component instead.
6
+
7
+ ## Usage
8
+
9
+ ```html
10
+ <button popovertarget="info-popup">Show Info</button>
11
+ <div id="info-popup" popover>
12
+ <p>This popup closes on click outside (light-dismiss).</p>
13
+ </div>
14
+ ```
15
+
16
+ ## CSS
17
+
18
+ ```css
19
+ [popover] {
20
+ padding: 16px;
21
+ border-radius: 8px;
22
+ border: 1px solid #ddd;
23
+ }
24
+ ```
25
+
26
+ Style with `:popover-open` for transitions:
27
+
28
+ ```css
29
+ [popover] {
30
+ opacity: 0;
31
+ transition: opacity 0.2s;
32
+ }
33
+ [popover]:popover-open {
34
+ opacity: 1;
35
+ }
36
+ ```
37
+
38
+ ## Accessibility
39
+
40
+ The Popover API handles focus management, Escape-to-close, and light-dismiss natively. `popovertarget` is keyboard-accessible (Enter/Space). No additional ARIA attributes needed for basic usage.
@@ -0,0 +1,45 @@
1
+ # Clipboard Copy
2
+
3
+ Copy-to-clipboard button with "Copied!" feedback. Headless component — requires import.
4
+
5
+ ## Import
6
+
7
+ ```html
8
+ <head>
9
+ <script
10
+ type="application/jay-headless"
11
+ plugin="@jay-framework/ui-kit"
12
+ contract="clipboard-copy"
13
+ ></script>
14
+ </head>
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <jay:clipboard-copy text="{shareUrl}">
21
+ <button ref="copyBtn" class="copy-btn">
22
+ <span if="!copied">Copy Link</span>
23
+ <span if="copied">Copied!</span>
24
+ </button>
25
+ </jay:clipboard-copy>
26
+ ```
27
+
28
+ **Required refs:** `copyBtn`
29
+
30
+ **Props:** `text` — the string to copy (bound from page ViewState)
31
+
32
+ **ViewState:** `copied` (boolean) — true for 2 seconds after clicking copy
33
+
34
+ ## Accessibility
35
+
36
+ Use `aria-live` to announce the copy feedback to screen readers:
37
+
38
+ ```html
39
+ <jay:clipboard-copy text="{shareUrl}">
40
+ <button ref="copyBtn" class="copy-btn">
41
+ <span if="!copied">Copy Link</span>
42
+ <span if="copied" aria-live="polite">Copied!</span>
43
+ </button>
44
+ </jay:clipboard-copy>
45
+ ```
@@ -0,0 +1,108 @@
1
+ # Letter Split
2
+
3
+ Splits dynamic text into one span per letter for individual character styling. Headless component — requires import.
4
+
5
+ ## Import
6
+
7
+ ```html
8
+ <head>
9
+ <script
10
+ type="application/jay-headless"
11
+ plugin="@jay-framework/ui-kit"
12
+ contract="letter-split"
13
+ ></script>
14
+ </head>
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <jay:letter-split text="{heroTitle}">
21
+ <span forEach="letters" trackBy="index" class="letter">{text}</span>
22
+ </jay:letter-split>
23
+ ```
24
+
25
+ **Props:** `text` — the string to split (bound from page ViewState)
26
+
27
+ **ViewState:** `letters` — array of `{ index: number, text: string }` (spaces are included as entries)
28
+
29
+ ## Accessibility & SEO
30
+
31
+ Screen readers will spell out each letter individually. Always add `aria-label` with the full text and hide the spans:
32
+
33
+ ```html
34
+ <jay:letter-split text="{heroTitle}">
35
+ <div aria-label="{heroTitle}" role="text">
36
+ <span forEach="letters" trackBy="index" class="letter" aria-hidden="true">{text}</span>
37
+ </div>
38
+ </jay:letter-split>
39
+ ```
40
+
41
+ For SEO, search engines read the text content of all spans combined — no impact on indexing.
42
+
43
+ ## Styling examples
44
+
45
+ Rainbow text:
46
+
47
+ ```css
48
+ .letter:nth-child(7n + 1) {
49
+ color: red;
50
+ }
51
+ .letter:nth-child(7n + 2) {
52
+ color: orange;
53
+ }
54
+ .letter:nth-child(7n + 3) {
55
+ color: yellow;
56
+ }
57
+ .letter:nth-child(7n + 4) {
58
+ color: green;
59
+ }
60
+ .letter:nth-child(7n + 5) {
61
+ color: blue;
62
+ }
63
+ .letter:nth-child(7n + 6) {
64
+ color: indigo;
65
+ }
66
+ .letter:nth-child(7n + 7) {
67
+ color: violet;
68
+ }
69
+ ```
70
+
71
+ Typewriter animation:
72
+
73
+ ```css
74
+ .letter {
75
+ display: inline-block;
76
+ opacity: 0;
77
+ animation: typeIn 0.05s forwards;
78
+ }
79
+ .letter:nth-child(1) {
80
+ animation-delay: 0s;
81
+ }
82
+ .letter:nth-child(2) {
83
+ animation-delay: 0.05s;
84
+ }
85
+ .letter:nth-child(3) {
86
+ animation-delay: 0.1s;
87
+ }
88
+ /* scale with calc or CSS custom properties */
89
+
90
+ @keyframes typeIn {
91
+ to {
92
+ opacity: 1;
93
+ }
94
+ }
95
+ ```
96
+
97
+ Hover effect per letter:
98
+
99
+ ```css
100
+ .letter {
101
+ display: inline-block;
102
+ transition: transform 0.2s;
103
+ }
104
+ .letter:hover {
105
+ transform: translateY(-4px) scale(1.2);
106
+ color: #e91e63;
107
+ }
108
+ ```
@@ -0,0 +1,99 @@
1
+ # Popover Menu
2
+
3
+ Dropdown menu that opens on hover. Headless component — requires import. For click-triggered popups, use `click-popover.md` instead (pure HTML).
4
+
5
+ ## Import
6
+
7
+ ```html
8
+ <head>
9
+ <script
10
+ type="application/jay-headless"
11
+ plugin="@jay-framework/ui-kit"
12
+ contract="popover-menu"
13
+ ></script>
14
+ </head>
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <jay:popover-menu>
21
+ <div class="nav-item">
22
+ <a ref="trigger" class="nav-link">Products &#x25BE;</a>
23
+ <div popover ref="popover" class="dropdown-menu">
24
+ <a href="/products/shoes">Shoes</a>
25
+ <a href="/products/bags">Bags</a>
26
+ </div>
27
+ </div>
28
+ </jay:popover-menu>
29
+ ```
30
+
31
+ **Required refs:** `trigger` (the hover target), `popover` (must have `popover` attribute)
32
+
33
+ ## CSS
34
+
35
+ Position the popover below the trigger using CSS Anchor Positioning. The component handles `showPopover()` on hover and provides a JS fallback for browsers without anchor support.
36
+
37
+ ```css
38
+ /* Anchor the trigger */
39
+ .nav-link {
40
+ anchor-name: --menu-trigger;
41
+ }
42
+
43
+ /* Position popover below the trigger */
44
+ .dropdown-menu {
45
+ position-anchor: --menu-trigger;
46
+ position: absolute;
47
+ inset: unset;
48
+ margin: 0;
49
+ top: anchor(bottom);
50
+ left: anchor(left);
51
+
52
+ /* Styling */
53
+ padding: 12px;
54
+ border-radius: 8px;
55
+ border: 1px solid #ddd;
56
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
57
+ opacity: 0;
58
+ transition: opacity 0.2s;
59
+ }
60
+ .dropdown-menu:popover-open {
61
+ opacity: 1;
62
+ }
63
+ ```
64
+
65
+ **Multiple menus:** Each trigger needs a unique `anchor-name`. Use different names per menu:
66
+
67
+ ```css
68
+ .products-trigger {
69
+ anchor-name: --products-menu;
70
+ }
71
+ .checkout-trigger {
72
+ anchor-name: --checkout-menu;
73
+ }
74
+ .products-dropdown {
75
+ position-anchor: --products-menu;
76
+ }
77
+ .checkout-dropdown {
78
+ position-anchor: --checkout-menu;
79
+ }
80
+ ```
81
+
82
+ **Browser support:** CSS Anchor Positioning works in Chrome 125+. The component automatically falls back to JS-based `position: fixed` positioning in browsers without support.
83
+
84
+ ## Accessibility
85
+
86
+ Add `aria-haspopup` and `aria-expanded` on the trigger. The Popover API handles focus trapping and Escape-to-close natively:
87
+
88
+ ```html
89
+ <a ref="trigger" class="nav-link" aria-haspopup="true" aria-expanded="false">Products &#x25BE;</a>
90
+ ```
91
+
92
+ Note: this component opens on hover only. Keyboard users can activate the popover via `popovertarget` (add it alongside the ref for click/keyboard access):
93
+
94
+ ```html
95
+ <a ref="trigger" class="nav-link" popovertarget="products-menu">Products &#x25BE;</a>
96
+ <div popover ref="popover" id="products-menu" class="dropdown-menu" role="menu">
97
+ <a href="/products/shoes" role="menuitem">Shoes</a>
98
+ </div>
99
+ ```
@@ -0,0 +1,67 @@
1
+ # Scroll Carousel
2
+
3
+ Horizontal slider with prev/next buttons and edge detection. Headless component — requires import.
4
+
5
+ ## Import
6
+
7
+ ```html
8
+ <head>
9
+ <script
10
+ type="application/jay-headless"
11
+ plugin="@jay-framework/ui-kit"
12
+ contract="scroll-carousel"
13
+ ></script>
14
+ </head>
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <jay:scroll-carousel>
21
+ <div class="carousel">
22
+ <button ref="prev" disabled="atStart" class="btn-prev">&#8249;</button>
23
+ <div ref="container" class="carousel-track">
24
+ <div class="slide">Slide 1</div>
25
+ <div class="slide">Slide 2</div>
26
+ <div class="slide">Slide 3</div>
27
+ </div>
28
+ <button ref="next" disabled="atEnd" class="btn-next">&#8250;</button>
29
+ </div>
30
+ </jay:scroll-carousel>
31
+ ```
32
+
33
+ **Required refs:** `container` (scrollable element), `prev`, `next` (buttons)
34
+
35
+ **ViewState:** `atStart` (boolean), `atEnd` (boolean) — use for disabling buttons or hiding arrows
36
+
37
+ ## Accessibility
38
+
39
+ Label the carousel region and navigation buttons:
40
+
41
+ ```html
42
+ <div class="carousel" role="region" aria-label="Product gallery">
43
+ <button ref="prev" disabled="atStart" aria-label="Previous slide">&#8249;</button>
44
+ <div ref="container" class="carousel-track">...</div>
45
+ <button ref="next" disabled="atEnd" aria-label="Next slide">&#8250;</button>
46
+ </div>
47
+ ```
48
+
49
+ All slides remain in the DOM (CSS scroll, not JS hide/show), so screen readers and search engines see all content.
50
+
51
+ ## CSS
52
+
53
+ The container must have scroll-snap:
54
+
55
+ ```css
56
+ .carousel-track {
57
+ display: flex;
58
+ overflow-x: auto;
59
+ scroll-snap-type: x mandatory;
60
+ scroll-behavior: smooth;
61
+ gap: 16px;
62
+ }
63
+ .slide {
64
+ scroll-snap-align: start;
65
+ flex: 0 0 100%; /* one slide per view, or adjust */
66
+ }
67
+ ```
@@ -0,0 +1,79 @@
1
+ # Tabs
2
+
3
+ Switch between content panels with tab buttons. Pure CSS using radio inputs, no component needed.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <div class="tabs">
9
+ <input type="radio" name="tab" id="tab-details" checked hidden />
10
+ <input type="radio" name="tab" id="tab-reviews" hidden />
11
+ <input type="radio" name="tab" id="tab-specs" hidden />
12
+
13
+ <nav class="tab-bar">
14
+ <label for="tab-details" class="tab-label">Details</label>
15
+ <label for="tab-reviews" class="tab-label">Reviews</label>
16
+ <label for="tab-specs" class="tab-label">Specs</label>
17
+ </nav>
18
+
19
+ <div class="tab-panels">
20
+ <div class="panel" id="panel-details">
21
+ <p>Product details go here...</p>
22
+ </div>
23
+ <div class="panel" id="panel-reviews">
24
+ <p>Customer reviews go here...</p>
25
+ </div>
26
+ <div class="panel" id="panel-specs">
27
+ <p>Technical specifications go here...</p>
28
+ </div>
29
+ </div>
30
+ </div>
31
+ ```
32
+
33
+ ## CSS
34
+
35
+ ```css
36
+ .panel {
37
+ display: none;
38
+ }
39
+
40
+ #tab-details:checked ~ .tab-panels #panel-details {
41
+ display: block;
42
+ }
43
+ #tab-reviews:checked ~ .tab-panels #panel-reviews {
44
+ display: block;
45
+ }
46
+ #tab-specs:checked ~ .tab-panels #panel-specs {
47
+ display: block;
48
+ }
49
+
50
+ .tab-label {
51
+ cursor: pointer;
52
+ padding: 8px 16px;
53
+ border-bottom: 2px solid transparent;
54
+ }
55
+ #tab-details:checked ~ .tab-bar label[for='tab-details'],
56
+ #tab-reviews:checked ~ .tab-bar label[for='tab-reviews'],
57
+ #tab-specs:checked ~ .tab-bar label[for='tab-specs'] {
58
+ border-bottom-color: #333;
59
+ font-weight: bold;
60
+ }
61
+ ```
62
+
63
+ **How it works:** Clicking a `<label>` checks its associated radio input. CSS `:checked` sibling selectors (`~`) show the matching panel.
64
+
65
+ ## Accessibility
66
+
67
+ The radio+label pattern is natively keyboard-accessible (arrow keys switch tabs). For screen reader semantics, add ARIA roles:
68
+
69
+ ```html
70
+ <nav class="tab-bar" role="tablist">
71
+ <label for="tab-details" class="tab-label" role="tab">Details</label>
72
+ <label for="tab-reviews" class="tab-label" role="tab">Reviews</label>
73
+ </nav>
74
+ <div class="tab-panels">
75
+ <div class="panel" id="panel-details" role="tabpanel">...</div>
76
+ </div>
77
+ ```
78
+
79
+ All panel content is in the DOM — search engines see everything regardless of which tab is active.
@@ -0,0 +1,92 @@
1
+ # Toggle Switch
2
+
3
+ On/off toggle styled as a switch. Pure CSS using a checkbox input, no component needed.
4
+
5
+ ## Usage
6
+
7
+ ```html
8
+ <label class="switch">
9
+ <input type="checkbox" />
10
+ <span class="slider"></span>
11
+ </label>
12
+ ```
13
+
14
+ With a label:
15
+
16
+ ```html
17
+ <label class="switch-row">
18
+ <span>Dark mode</span>
19
+ <span class="switch">
20
+ <input type="checkbox" />
21
+ <span class="slider"></span>
22
+ </span>
23
+ </label>
24
+ ```
25
+
26
+ ## CSS
27
+
28
+ ```css
29
+ .switch {
30
+ position: relative;
31
+ display: inline-block;
32
+ width: 44px;
33
+ height: 24px;
34
+ }
35
+ .switch input {
36
+ opacity: 0;
37
+ width: 0;
38
+ height: 0;
39
+ }
40
+ .slider {
41
+ position: absolute;
42
+ inset: 0;
43
+ background: #ccc;
44
+ border-radius: 24px;
45
+ cursor: pointer;
46
+ transition: background 0.2s;
47
+ }
48
+ .slider::before {
49
+ content: '';
50
+ position: absolute;
51
+ width: 18px;
52
+ height: 18px;
53
+ left: 3px;
54
+ bottom: 3px;
55
+ background: white;
56
+ border-radius: 50%;
57
+ transition: transform 0.2s;
58
+ }
59
+ input:checked + .slider {
60
+ background: #4caf50;
61
+ }
62
+ input:checked + .slider::before {
63
+ transform: translateX(20px);
64
+ }
65
+ ```
66
+
67
+ ## With layout
68
+
69
+ ```css
70
+ .switch-row {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 12px;
74
+ cursor: pointer;
75
+ }
76
+ ```
77
+
78
+ **How it works:** A hidden checkbox drives the state. CSS `:checked` selector moves the slider knob and changes the background color.
79
+
80
+ ## Accessibility
81
+
82
+ The hidden checkbox is keyboard-accessible (Space to toggle) when wrapped in a `<label>`. Add `role="switch"` for screen readers:
83
+
84
+ ```html
85
+ <label class="switch-row">
86
+ <span>Dark mode</span>
87
+ <span class="switch">
88
+ <input type="checkbox" role="switch" />
89
+ <span class="slider"></span>
90
+ </span>
91
+ </label>
92
+ ```
@@ -0,0 +1,47 @@
1
+ # Tooltip
2
+
3
+ Hover tooltip showing extra info. Pure CSS using `::after` pseudo-element, no component needed.
4
+
5
+ **Accessibility note:** CSS `::after` content is NOT read by most screen readers. Use `title` or `aria-label` for the accessible version of the tooltip text:
6
+
7
+ ## Usage
8
+
9
+ ```html
10
+ <span
11
+ class="has-tooltip"
12
+ data-tooltip="Ships in 2-3 business days"
13
+ aria-label="Ships in 2-3 business days"
14
+ >
15
+ Shipping info &#9432;
16
+ </span>
17
+ ```
18
+
19
+ The `data-tooltip` drives the CSS visual tooltip. The `aria-label` makes the same text available to screen readers.
20
+
21
+ ## CSS
22
+
23
+ ```css
24
+ .has-tooltip {
25
+ position: relative;
26
+ cursor: help;
27
+ }
28
+ .has-tooltip::after {
29
+ content: attr(data-tooltip);
30
+ position: absolute;
31
+ bottom: calc(100% + 6px);
32
+ left: 50%;
33
+ transform: translateX(-50%);
34
+ padding: 6px 10px;
35
+ background: #333;
36
+ color: white;
37
+ border-radius: 4px;
38
+ font-size: 13px;
39
+ white-space: nowrap;
40
+ opacity: 0;
41
+ pointer-events: none;
42
+ transition: opacity 0.2s;
43
+ }
44
+ .has-tooltip:hover::after {
45
+ opacity: 1;
46
+ }
47
+ ```
@@ -0,0 +1,82 @@
1
+ # Word Split
2
+
3
+ Splits dynamic text into one span per word for individual word styling. Headless component — requires import.
4
+
5
+ ## Import
6
+
7
+ ```html
8
+ <head>
9
+ <script
10
+ type="application/jay-headless"
11
+ plugin="@jay-framework/ui-kit"
12
+ contract="word-split"
13
+ ></script>
14
+ </head>
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <jay:word-split text="{title}">
21
+ <span forEach="words" trackBy="index" class="word">{text} </span>
22
+ </jay:word-split>
23
+ ```
24
+
25
+ **Props:** `text` — the string to split (bound from page ViewState)
26
+
27
+ **ViewState:** `words` — array of `{ index: number, text: string }`
28
+
29
+ ## Accessibility & SEO
30
+
31
+ Screen readers may pause between spans, making split text sound unnatural. Add `aria-label` with the full text on the container and hide the individual spans:
32
+
33
+ ```html
34
+ <jay:word-split text="{title}">
35
+ <div aria-label="{title}" role="text">
36
+ <span forEach="words" trackBy="index" class="word" aria-hidden="true">{text} </span>
37
+ </div>
38
+ </jay:word-split>
39
+ ```
40
+
41
+ For SEO, the full text is still in the HTML (each word in a span) — search engines read it normally. The `aria-label` ensures assistive technology reads the unsplit version.
42
+
43
+ ## Styling examples
44
+
45
+ Highlight every other word:
46
+
47
+ ```css
48
+ .word:nth-child(odd) {
49
+ color: #e91e63;
50
+ }
51
+ ```
52
+
53
+ Stagger animation per word:
54
+
55
+ ```css
56
+ .word {
57
+ display: inline-block;
58
+ animation: fadeIn 0.3s both;
59
+ }
60
+ .word:nth-child(1) {
61
+ animation-delay: 0s;
62
+ }
63
+ .word:nth-child(2) {
64
+ animation-delay: 0.1s;
65
+ }
66
+ .word:nth-child(3) {
67
+ animation-delay: 0.2s;
68
+ }
69
+ /* or use calc: animation-delay: calc(var(--index) * 0.1s); */
70
+ ```
71
+
72
+ Style a specific word by position:
73
+
74
+ ```css
75
+ .word:first-child {
76
+ font-size: 2em;
77
+ font-weight: bold;
78
+ }
79
+ .word:last-child {
80
+ font-style: italic;
81
+ }
82
+ ```
@@ -0,0 +1,21 @@
1
+ name: clipboard-copy
2
+ props:
3
+ - name: text
4
+ kind: required
5
+ tags:
6
+ - tag: text
7
+ type: data
8
+ dataType: string
9
+ phase: fast+interactive
10
+ description: Text to copy (from props)
11
+
12
+ - tag: copied
13
+ type: data
14
+ dataType: boolean
15
+ phase: fast+interactive
16
+ description: True for a short period after copying
17
+
18
+ - tag: copyBtn
19
+ type: interactive
20
+ elementType: HTMLButtonElement
21
+ description: Button that triggers the copy
@@ -0,0 +1,29 @@
1
+ import {HTMLElementCollectionProxy, HTMLElementProxy, JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface ClipboardCopyViewState {
5
+ text: string,
6
+ copied: boolean
7
+ }
8
+
9
+ export type ClipboardCopySlowViewState = {};
10
+
11
+ export type ClipboardCopyFastViewState = Pick<ClipboardCopyViewState, 'text' | 'copied'>;
12
+
13
+ export type ClipboardCopyInteractiveViewState = Pick<ClipboardCopyViewState, 'text' | 'copied'>;
14
+
15
+
16
+ export interface ClipboardCopyRefs {
17
+ copyBtn: HTMLElementProxy<ClipboardCopyViewState, HTMLButtonElement>
18
+ }
19
+
20
+
21
+ export interface ClipboardCopyRepeatedRefs {
22
+ copyBtn: HTMLElementCollectionProxy<ClipboardCopyViewState, HTMLButtonElement>
23
+ }
24
+
25
+ export interface ClipboardCopyProps {
26
+ text?: string;
27
+ }
28
+
29
+ export type ClipboardCopyContract = JayContract<ClipboardCopyViewState, ClipboardCopyRefs, ClipboardCopySlowViewState, ClipboardCopyFastViewState, ClipboardCopyInteractiveViewState, ClipboardCopyProps>
@@ -0,0 +1,83 @@
1
+ import { makeJayStackComponent } from "@jay-framework/fullstack-component";
2
+ import { createSignal } from "@jay-framework/component";
3
+ const supportsAnchor = typeof CSS !== "undefined" && CSS.supports("anchor-name", "--x");
4
+ const popoverMenu = makeJayStackComponent().withProps().withInteractive(function PopoverMenu(props, refs) {
5
+ refs.trigger.onmouseenter(async () => {
6
+ if (!supportsAnchor) {
7
+ const rect = await refs.trigger.exec$((el) => {
8
+ return el.getBoundingClientRect();
9
+ });
10
+ refs.popover.exec$((el) => {
11
+ el.style.position = "fixed";
12
+ el.style.inset = "unset";
13
+ el.style.margin = "0";
14
+ el.style.top = `${rect.bottom}px`;
15
+ el.style.left = `${rect.left}px`;
16
+ });
17
+ }
18
+ refs.popover.exec$((el) => {
19
+ el.showPopover();
20
+ });
21
+ });
22
+ return { render: () => ({}) };
23
+ });
24
+ const scrollCarousel = makeJayStackComponent().withProps().withInteractive(function ScrollCarousel(props, refs) {
25
+ const [atStart, setAtStart] = createSignal(true);
26
+ const [atEnd, setAtEnd] = createSignal(false);
27
+ refs.prev.onclick(() => {
28
+ refs.container.exec$((el) => {
29
+ el.scrollBy({ left: -el.clientWidth, behavior: "smooth" });
30
+ });
31
+ });
32
+ refs.next.onclick(() => {
33
+ refs.container.exec$((el) => {
34
+ el.scrollBy({ left: el.clientWidth, behavior: "smooth" });
35
+ });
36
+ });
37
+ const updateScrollState = () => {
38
+ refs.container.exec$((el) => {
39
+ setAtStart(el.scrollLeft <= 0);
40
+ setAtEnd(el.scrollLeft + el.clientWidth >= el.scrollWidth - 1);
41
+ });
42
+ };
43
+ refs.container.onscroll(updateScrollState);
44
+ return {
45
+ render: () => ({
46
+ atStart: atStart(),
47
+ atEnd: atEnd()
48
+ })
49
+ };
50
+ });
51
+ const clipboardCopy = makeJayStackComponent().withProps().withInteractive(function ClipboardCopy(props, refs) {
52
+ const [copied, setCopied] = createSignal(false);
53
+ refs.copyBtn.onclick(async () => {
54
+ await navigator.clipboard.writeText(props.text());
55
+ setCopied(true);
56
+ setTimeout(() => setCopied(false), 2e3);
57
+ });
58
+ return {
59
+ render: () => ({
60
+ text: props.text(),
61
+ copied: copied()
62
+ })
63
+ };
64
+ });
65
+ function splitWords(text) {
66
+ return text.split(/\s+/).filter(Boolean).map((word, i) => ({ index: i, text: word }));
67
+ }
68
+ const wordSplit = makeJayStackComponent().withProps().withInteractive((props) => ({
69
+ render: () => ({ words: splitWords(props.text()) })
70
+ }));
71
+ function splitLetters(text) {
72
+ return [...text].map((char, i) => ({ index: i, text: char }));
73
+ }
74
+ const letterSplit = makeJayStackComponent().withProps().withInteractive((props) => ({
75
+ render: () => ({ letters: splitLetters(props.text()) })
76
+ }));
77
+ export {
78
+ clipboardCopy,
79
+ letterSplit,
80
+ popoverMenu,
81
+ scrollCarousel,
82
+ wordSplit
83
+ };
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { makeJayStackComponent, phaseOutput } from "@jay-framework/fullstack-component";
2
+ const popoverMenu = makeJayStackComponent().withProps();
3
+ const scrollCarousel = makeJayStackComponent().withProps().withFastRender(async () => phaseOutput({ atStart: true, atEnd: false }, {}));
4
+ const clipboardCopy = makeJayStackComponent().withProps().withFastRender(async (props) => phaseOutput({ text: props.text ?? "", copied: false }, {}));
5
+ function splitWords(text) {
6
+ return text.split(/\s+/).filter(Boolean).map((word, i) => ({ index: i, text: word }));
7
+ }
8
+ const wordSplit = makeJayStackComponent().withProps().withFastRender(async (props) => phaseOutput({ words: splitWords(props.text ?? "") }, {}));
9
+ function splitLetters(text) {
10
+ return [...text].map((char, i) => ({ index: i, text: char }));
11
+ }
12
+ const letterSplit = makeJayStackComponent().withProps().withFastRender(async (props) => phaseOutput({ letters: splitLetters(props.text ?? "") }, {}));
13
+ export {
14
+ clipboardCopy,
15
+ letterSplit,
16
+ popoverMenu,
17
+ scrollCarousel,
18
+ wordSplit
19
+ };
@@ -0,0 +1,17 @@
1
+ name: letter-split
2
+ props:
3
+ - name: text
4
+ kind: required
5
+ tags:
6
+ - tag: letters
7
+ type: sub-contract
8
+ repeated: true
9
+ trackBy: index
10
+ phase: fast+interactive
11
+ tags:
12
+ - tag: index
13
+ type: data
14
+ dataType: number
15
+ - tag: text
16
+ type: data
17
+ dataType: string
@@ -0,0 +1,31 @@
1
+ import {JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface LetterOfLetterSplitViewState {
5
+ index: number,
6
+ text: string
7
+ }
8
+
9
+ export interface LetterSplitViewState {
10
+ letters: Array<LetterOfLetterSplitViewState>
11
+ }
12
+
13
+ export type LetterSplitSlowViewState = {};
14
+
15
+ export type LetterSplitFastViewState = {
16
+ letters: Array<LetterSplitViewState['letters'][number]>;
17
+ };
18
+
19
+ export type LetterSplitInteractiveViewState = {
20
+ letters: Array<LetterSplitViewState['letters'][number]>;
21
+ };
22
+
23
+ export interface LetterSplitRefs {}
24
+
25
+ export interface LetterSplitRepeatedRefs {}
26
+
27
+ export interface LetterSplitProps {
28
+ text?: string;
29
+ }
30
+
31
+ export type LetterSplitContract = JayContract<LetterSplitViewState, LetterSplitRefs, LetterSplitSlowViewState, LetterSplitFastViewState, LetterSplitInteractiveViewState, LetterSplitProps>
@@ -0,0 +1,11 @@
1
+ name: popover-menu
2
+ tags:
3
+ - tag: trigger
4
+ type: interactive
5
+ elementType: HTMLElement
6
+ description: Element that opens the popover on hover
7
+
8
+ - tag: popover
9
+ type: interactive
10
+ elementType: HTMLElement
11
+ description: Element with popover attribute to show/hide
@@ -0,0 +1,24 @@
1
+ import {HTMLElementCollectionProxy, HTMLElementProxy, JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface PopoverMenuViewState {}
5
+
6
+ export type PopoverMenuSlowViewState = {};
7
+
8
+ export type PopoverMenuFastViewState = {};
9
+
10
+ export type PopoverMenuInteractiveViewState = {};
11
+
12
+
13
+ export interface PopoverMenuRefs {
14
+ trigger: HTMLElementProxy<PopoverMenuViewState, HTMLElement>,
15
+ popover: HTMLElementProxy<PopoverMenuViewState, HTMLElement>
16
+ }
17
+
18
+
19
+ export interface PopoverMenuRepeatedRefs {
20
+ trigger: HTMLElementCollectionProxy<PopoverMenuViewState, HTMLElement>,
21
+ popover: HTMLElementCollectionProxy<PopoverMenuViewState, HTMLElement>
22
+ }
23
+
24
+ export type PopoverMenuContract = JayContract<PopoverMenuViewState, PopoverMenuRefs, PopoverMenuSlowViewState, PopoverMenuFastViewState, PopoverMenuInteractiveViewState>
@@ -0,0 +1,28 @@
1
+ name: scroll-carousel
2
+ tags:
3
+ - tag: container
4
+ type: interactive
5
+ elementType: HTMLElement
6
+ description: Scrollable container with overflow-x auto and scroll-snap
7
+
8
+ - tag: prev
9
+ type: interactive
10
+ elementType: HTMLButtonElement
11
+ description: Scroll to previous item
12
+
13
+ - tag: next
14
+ type: interactive
15
+ elementType: HTMLButtonElement
16
+ description: Scroll to next item
17
+
18
+ - tag: atStart
19
+ type: data
20
+ dataType: boolean
21
+ phase: fast+interactive
22
+ description: True when scrolled to the beginning
23
+
24
+ - tag: atEnd
25
+ type: data
26
+ dataType: boolean
27
+ phase: fast+interactive
28
+ description: True when scrolled to the end
@@ -0,0 +1,29 @@
1
+ import {HTMLElementCollectionProxy, HTMLElementProxy, JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface ScrollCarouselViewState {
5
+ atStart: boolean,
6
+ atEnd: boolean
7
+ }
8
+
9
+ export type ScrollCarouselSlowViewState = {};
10
+
11
+ export type ScrollCarouselFastViewState = Pick<ScrollCarouselViewState, 'atStart' | 'atEnd'>;
12
+
13
+ export type ScrollCarouselInteractiveViewState = Pick<ScrollCarouselViewState, 'atStart' | 'atEnd'>;
14
+
15
+
16
+ export interface ScrollCarouselRefs {
17
+ container: HTMLElementProxy<ScrollCarouselViewState, HTMLElement>,
18
+ prev: HTMLElementProxy<ScrollCarouselViewState, HTMLButtonElement>,
19
+ next: HTMLElementProxy<ScrollCarouselViewState, HTMLButtonElement>
20
+ }
21
+
22
+
23
+ export interface ScrollCarouselRepeatedRefs {
24
+ container: HTMLElementCollectionProxy<ScrollCarouselViewState, HTMLElement>,
25
+ prev: HTMLElementCollectionProxy<ScrollCarouselViewState, HTMLButtonElement>,
26
+ next: HTMLElementCollectionProxy<ScrollCarouselViewState, HTMLButtonElement>
27
+ }
28
+
29
+ export type ScrollCarouselContract = JayContract<ScrollCarouselViewState, ScrollCarouselRefs, ScrollCarouselSlowViewState, ScrollCarouselFastViewState, ScrollCarouselInteractiveViewState>
@@ -0,0 +1,17 @@
1
+ name: word-split
2
+ props:
3
+ - name: text
4
+ kind: required
5
+ tags:
6
+ - tag: words
7
+ type: sub-contract
8
+ repeated: true
9
+ trackBy: index
10
+ phase: fast+interactive
11
+ tags:
12
+ - tag: index
13
+ type: data
14
+ dataType: number
15
+ - tag: text
16
+ type: data
17
+ dataType: string
@@ -0,0 +1,31 @@
1
+ import {JayContract} from "@jay-framework/runtime";
2
+
3
+
4
+ export interface WordOfWordSplitViewState {
5
+ index: number,
6
+ text: string
7
+ }
8
+
9
+ export interface WordSplitViewState {
10
+ words: Array<WordOfWordSplitViewState>
11
+ }
12
+
13
+ export type WordSplitSlowViewState = {};
14
+
15
+ export type WordSplitFastViewState = {
16
+ words: Array<WordSplitViewState['words'][number]>;
17
+ };
18
+
19
+ export type WordSplitInteractiveViewState = {
20
+ words: Array<WordSplitViewState['words'][number]>;
21
+ };
22
+
23
+ export interface WordSplitRefs {}
24
+
25
+ export interface WordSplitRepeatedRefs {}
26
+
27
+ export interface WordSplitProps {
28
+ text?: string;
29
+ }
30
+
31
+ export type WordSplitContract = JayContract<WordSplitViewState, WordSplitRefs, WordSplitSlowViewState, WordSplitFastViewState, WordSplitInteractiveViewState, WordSplitProps>
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@jay-framework/ui-kit",
3
+ "version": "0.16.4",
4
+ "type": "module",
5
+ "description": "Headless UI primitives: popover menu, scroll carousel, clipboard copy",
6
+ "license": "Apache-2.0",
7
+ "main": "dist/index.js",
8
+ "files": [
9
+ "dist",
10
+ "plugin.yaml",
11
+ "agent-kit"
12
+ ],
13
+ "exports": {
14
+ ".": "./dist/index.js",
15
+ "./client": "./dist/index.client.js",
16
+ "./plugin.yaml": "./plugin.yaml",
17
+ "./popover-menu.jay-contract": "./dist/popover-menu.jay-contract",
18
+ "./scroll-carousel.jay-contract": "./dist/scroll-carousel.jay-contract",
19
+ "./clipboard-copy.jay-contract": "./dist/clipboard-copy.jay-contract",
20
+ "./word-split.jay-contract": "./dist/word-split.jay-contract",
21
+ "./letter-split.jay-contract": "./dist/letter-split.jay-contract"
22
+ },
23
+ "scripts": {
24
+ "build": "npm run definitions && npm run build:client && npm run build:server && npm run build:copy-assets",
25
+ "definitions": "jay-cli definitions lib",
26
+ "build:client": "vite build",
27
+ "build:server": "vite build --ssr",
28
+ "build:copy-assets": "cp lib/*.jay-contract* dist/",
29
+ "build:check-types": "tsc",
30
+ "clean": "rimraf dist",
31
+ "confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
32
+ "test": ":"
33
+ },
34
+ "dependencies": {
35
+ "@jay-framework/component": "^0.16.4",
36
+ "@jay-framework/fullstack-component": "^0.16.4",
37
+ "@jay-framework/reactive": "^0.16.4",
38
+ "@jay-framework/runtime": "^0.16.4",
39
+ "@jay-framework/stack-client-runtime": "^0.16.4"
40
+ },
41
+ "devDependencies": {
42
+ "@jay-framework/compiler-jay-stack": "^0.16.4",
43
+ "@jay-framework/dev-environment": "^0.16.4",
44
+ "@jay-framework/jay-cli": "^0.16.4",
45
+ "rimraf": "^5.0.5",
46
+ "typescript": "^5.3.3",
47
+ "vite": "^5.0.11"
48
+ }
49
+ }
package/plugin.yaml ADDED
@@ -0,0 +1,26 @@
1
+ name: ui-kit
2
+ contracts:
3
+ - name: popover-menu
4
+ contract: popover-menu.jay-contract
5
+ component: popoverMenu
6
+ description: Hover-triggered popover using the Popover API — adds showPopover() on mouseenter
7
+
8
+ - name: scroll-carousel
9
+ contract: scroll-carousel.jay-contract
10
+ component: scrollCarousel
11
+ description: Prev/next scroll buttons for CSS scroll-snap containers — tracks atStart/atEnd
12
+
13
+ - name: clipboard-copy
14
+ contract: clipboard-copy.jay-contract
15
+ component: clipboardCopy
16
+ description: Copy text to clipboard with visual feedback — toggles copied state for 2 seconds
17
+
18
+ - name: word-split
19
+ contract: word-split.jay-contract
20
+ component: wordSplit
21
+ description: Splits text into a span per word for individual word styling
22
+
23
+ - name: letter-split
24
+ contract: letter-split.jay-contract
25
+ component: letterSplit
26
+ description: Splits text into a span per letter for individual letter styling