@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.
- package/agent-kit/designer/accordion.md +84 -0
- package/agent-kit/designer/click-popover.md +40 -0
- package/agent-kit/designer/clipboard-copy.md +45 -0
- package/agent-kit/designer/letter-split.md +108 -0
- package/agent-kit/designer/popover-menu.md +99 -0
- package/agent-kit/designer/scroll-carousel.md +67 -0
- package/agent-kit/designer/tabs.md +79 -0
- package/agent-kit/designer/toggle-switch.md +92 -0
- package/agent-kit/designer/tooltip.md +47 -0
- package/agent-kit/designer/word-split.md +82 -0
- package/dist/clipboard-copy.jay-contract +21 -0
- package/dist/clipboard-copy.jay-contract.d.ts +29 -0
- package/dist/index.client.js +83 -0
- package/dist/index.js +19 -0
- package/dist/letter-split.jay-contract +17 -0
- package/dist/letter-split.jay-contract.d.ts +31 -0
- package/dist/popover-menu.jay-contract +11 -0
- package/dist/popover-menu.jay-contract.d.ts +24 -0
- package/dist/scroll-carousel.jay-contract +28 -0
- package/dist/scroll-carousel.jay-contract.d.ts +29 -0
- package/dist/word-split.jay-contract +17 -0
- package/dist/word-split.jay-contract.d.ts +31 -0
- package/package.json +49 -0
- package/plugin.yaml +26 -0
|
@@ -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 ▾</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 ▾</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 ▾</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">‹</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">›</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">‹</button>
|
|
44
|
+
<div ref="container" class="carousel-track">...</div>
|
|
45
|
+
<button ref="next" disabled="atEnd" aria-label="Next slide">›</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 ⓘ
|
|
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
|