-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathscript.js
More file actions
202 lines (187 loc) · 7.33 KB
/
Copy pathscript.js
File metadata and controls
202 lines (187 loc) · 7.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// Home-page interactions: contribute form, accordions, scroll reveals.
// ---------------------------------------------------------------
// Contribute CTA: reveal the form, submit via the user's mail app
// ---------------------------------------------------------------
const contribBtn = document.getElementById('contrib-btn');
const contribForm = document.getElementById('contrib-form');
function openContribute(focus = true) {
if (!contribForm) return;
contribForm.hidden = false;
contribBtn.setAttribute('aria-expanded', 'true');
if (focus) contribForm.querySelector('input[name="name"]').focus();
}
if (contribBtn && contribForm) {
contribBtn.addEventListener('click', () => {
if (contribForm.hidden) openContribute();
else {
contribForm.hidden = true;
contribBtn.setAttribute('aria-expanded', 'false');
}
});
contribForm.addEventListener('submit', (e) => {
e.preventDefault();
const name = contribForm.elements.name.value.trim();
const message = contribForm.elements.message.value.trim();
const subject = 'Contributing to the Agentic Interface Framework';
const body = (name ? 'Name: ' + name + '\n\n' : '') + message;
window.location.href =
'mailto:parikshit@thesys.dev?subject=' + encodeURIComponent(subject) +
'&body=' + encodeURIComponent(body);
});
// Deep link: arriving at ./#contribute opens the form in place.
if (location.hash === '#contribute') openContribute(false);
}
// ---------------------------------------------------------------
// Accordions: exclusive open, animated as expand-then-fade.
// The panel's height eases open first; once it has expanded, the
// content fades in. Collapse animates height and opacity together.
// With prefers-reduced-motion (or no JS) the native details
// behavior and its name="" exclusivity apply instead.
// ---------------------------------------------------------------
const accItems = Array.from(document.querySelectorAll('details.acc-item'));
const ACC_REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const ACC_EASE =
getComputedStyle(document.documentElement).getPropertyValue('--ease-out').trim() ||
'cubic-bezier(0.16, 1, 0.3, 1)';
// Runs `done` exactly once: on transitionend, or after `ms` as a
// fallback (throttled renderers may never deliver the event).
function accSettle(el, ms, done) {
let fired = false;
const finish = () => {
if (fired) return;
fired = true;
done();
};
const onEnd = (ev) => {
if (ev.target !== el) return;
el.removeEventListener('transitionend', onEnd);
finish();
};
el.addEventListener('transitionend', onEnd);
setTimeout(finish, ms);
}
function accExpand(item) {
const body = item.querySelector('.acc-body');
item.dataset.animating = '1';
item.open = true;
const target = body.scrollHeight;
body.style.overflow = 'hidden';
body.style.height = '0px';
body.style.opacity = '0';
void body.offsetHeight;
body.style.transition = 'height 320ms ' + ACC_EASE;
body.style.height = target + 'px';
accSettle(body, 400, () => {
body.style.transition = 'opacity 260ms ease';
body.style.height = '';
body.style.overflow = '';
void body.offsetHeight;
body.style.opacity = '1';
accSettle(body, 330, () => {
body.style.cssText = '';
delete item.dataset.animating;
// A sibling asked this panel to close while it was mid-expand
// (exclusivity would otherwise leave two panels open).
if (item.dataset.closeAfter) {
delete item.dataset.closeAfter;
accCollapse(item);
}
});
});
}
function accCollapse(item) {
const body = item.querySelector('.acc-body');
item.dataset.animating = '1';
body.style.overflow = 'hidden';
body.style.height = body.scrollHeight + 'px';
void body.offsetHeight;
body.style.transition = 'height 260ms ' + ACC_EASE + ', opacity 180ms ease';
body.style.height = '0px';
body.style.opacity = '0';
accSettle(body, 340, () => {
item.open = false;
body.style.cssText = '';
delete item.dataset.animating;
});
}
// Closes `item` instantly and keeps `anchorEl` (the clicked summary)
// at the same viewport position. Used for open siblings ABOVE the
// clicked row: an animated collapse there would drag the clicked row
// up under the cursor, which reads as a scroll glitch. The correction
// is the row's measured displacement, so any scroll clamping the
// browser applies when the document shrinks is accounted for.
function accCollapseAbove(item, anchorEl) {
const body = item.querySelector('.acc-body');
body.style.cssText = '';
delete item.dataset.animating;
delete item.dataset.closeAfter;
const before = anchorEl.getBoundingClientRect().top;
item.open = false;
const shift = anchorEl.getBoundingClientRect().top - before;
if (shift) window.scrollBy(0, shift);
}
if (accItems.length && !ACC_REDUCED) {
// JS drives exclusivity so sibling closes can animate; the native
// exclusive-accordion attribute would snap them shut instead.
accItems.forEach((d) => d.removeAttribute('name'));
accItems.forEach((item) => {
const summary = item.querySelector('summary');
const body = item.querySelector('.acc-body');
if (!summary || !body) return;
summary.addEventListener('click', (e) => {
e.preventDefault();
if (item.dataset.animating) return;
if (item.open) {
accCollapse(item);
} else {
accItems.forEach((other) => {
if (other === item || !other.open) return;
// "other precedes item": collapse instantly with scroll
// compensation so the clicked row stays pinned; siblings
// below can animate freely without shifting the row.
if (other.compareDocumentPosition(item) & Node.DOCUMENT_POSITION_FOLLOWING) {
accCollapseAbove(other, summary);
} else if (other.dataset.animating) {
other.dataset.closeAfter = '1';
} else {
accCollapse(other);
}
});
accExpand(item);
}
});
});
}
// ---------------------------------------------------------------
// Scroll-entry reveals, with a hard fallback: if IntersectionObserver
// is unavailable, everything is shown immediately.
// ---------------------------------------------------------------
const reveals = document.querySelectorAll('.reveal');
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
entry.target.classList.add('in');
observer.unobserve(entry.target);
}
}
},
{ threshold: 0.08, rootMargin: '0px 0px -6% 0px' }
);
reveals.forEach((el) => observer.observe(el));
// Safety net: some environments (embedded webviews, throttled background
// renderers) stop delivering observer callbacks and stall CSS transitions.
// Nothing may stay hidden because an animation couldn't run, so after a
// beat reveal everything with the transition disabled — elements the
// observer already handled are unaffected. (styles.css carries a second,
// pure-CSS failsafe for the case where this file never runs at all.)
setTimeout(() => {
reveals.forEach((el) => {
if (!el.classList.contains('in')) el.style.transition = 'none';
el.classList.add('in');
});
}, 1800);
} else {
reveals.forEach((el) => el.classList.add('in'));
}