g1t/apps/docs/src/components/PageTitle.astro

212 lines5,616 bytesCodeBlame
1---
2// A page's title, what it is about beneath it, and a way to take the page
3// to an AI assistant: copied as Markdown, read as Markdown, or opened in
4// Claude or ChatGPT.
5import { ArrowUpRight, ChevronDown, Copy, FileText, MessageSquare } from '@lucide/astro';
6
7const { entry, id } = Astro.locals.starlightRoute;
8const { title, description } = entry.data;
9const markdown = `/${id === '' || id === 'index' ? 'index' : id}.md`;
10const absolute = new URL(markdown, Astro.site).href;
11const ask = encodeURIComponent(`Read ${absolute} so I can ask questions about it.`);
12---
13
14<div class="g1t-page-title">
15 <div class="g1t-page-title-row">
16 <h1 id="_top">{title}</h1>
17 <div class="g1t-copy not-content">
18 <button type="button" class="g1t-copy-main" data-markdown={markdown}>
19 <Copy />
20 <span>Copy page</span>
21 </button>
22 <details class="g1t-copy-more">
23 <summary aria-label="More ways to use this page">
24 <ChevronDown />
25 </summary>
26 <div class="g1t-copy-menu">
27 <a href={markdown} target="_blank" rel="noopener">
28 <FileText class="g1t-copy-icon" />
29 <span class="g1t-copy-text"><strong>View as Markdown</strong><small>This page as plain text</small></span>
30 </a>
31 <a href={`https://claude.ai/new?q=${ask}`} target="_blank" rel="noopener">
32 <MessageSquare class="g1t-copy-icon" />
33 <span class="g1t-copy-text"><strong>Open in Claude</strong><small>Ask questions about this page</small></span>
34 <ArrowUpRight class="g1t-copy-out" />
35 </a>
36 <a href={`https://chatgpt.com/?hints=search&q=${ask}`} target="_blank" rel="noopener">
37 <MessageSquare class="g1t-copy-icon" />
38 <span class="g1t-copy-text"><strong>Open in ChatGPT</strong><small>Ask questions about this page</small></span>
39 <ArrowUpRight class="g1t-copy-out" />
40 </a>
41 </div>
42 </details>
43 </div>
44 </div>
45 {description && <p class="g1t-description">{description}</p>}
46</div>
47
48<script>
49 document.querySelectorAll<HTMLButtonElement>('.g1t-copy-main').forEach((button) => {
50 button.addEventListener('click', async () => {
51 const label = button.querySelector('span')!;
52 try {
53 const text = await (await fetch(button.dataset.markdown!)).text();
54 await navigator.clipboard.writeText(text);
55 label.textContent = 'Copied';
56 } catch {
57 label.textContent = 'Could not copy';
58 }
59 setTimeout(() => (label.textContent = 'Copy page'), 1600);
60 });
61 });
62 // A menu closes when you click elsewhere.
63 document.addEventListener('click', (event) => {
64 document.querySelectorAll<HTMLDetailsElement>('.g1t-copy-more[open]').forEach((menu) => {
65 if (!menu.contains(event.target as Node)) menu.open = false;
66 });
67 });
68</script>
69
70<style>
71 .g1t-page-title {
72 margin-top: 0.75rem;
73 }
74 .g1t-page-title-row {
75 display: flex;
76 align-items: flex-start;
77 justify-content: space-between;
78 gap: 1.5rem;
79 }
80 h1 {
81 margin: 0;
82 font-family: var(--g1t-font-display);
83 font-optical-sizing: auto;
84 font-size: 2rem;
85 font-weight: 600;
86 letter-spacing: -0.015em;
87 line-height: 1.2;
88 color: var(--g1t-fg);
89 }
90 .g1t-description {
91 margin-top: 0.6rem;
92 max-width: 42rem;
93 font-size: 1.125rem;
94 line-height: 1.55;
95 color: var(--g1t-muted);
96 }
97 .g1t-copy {
98 position: relative;
99 display: flex;
100 flex-shrink: 0;
101 margin-top: 0.35rem;
102 border-radius: 0.5rem;
103 box-shadow: inset 0 0 0 1px var(--g1t-line);
104 }
105 .g1t-copy :global(svg) {
106 width: 0.9rem;
107 height: 0.9rem;
108 }
109 .g1t-copy-main,
110 .g1t-copy-more summary {
111 display: flex;
112 align-items: center;
113 gap: 0.4rem;
114 border: 0;
115 background: transparent;
116 padding: 0.3rem 0.6rem;
117 font: inherit;
118 font-size: 0.8125rem;
119 color: var(--g1t-muted);
120 cursor: pointer;
121 transition: color 0.15s, background 0.15s;
122 }
123 .g1t-copy-main {
124 border-radius: 0.5rem 0 0 0.5rem;
125 }
126 .g1t-copy-more summary {
127 list-style: none;
128 border-left: 1px solid var(--g1t-line);
129 border-radius: 0 0.5rem 0.5rem 0;
130 padding-inline: 0.45rem;
131 }
132 .g1t-copy-more summary::-webkit-details-marker {
133 display: none;
134 }
135 .g1t-copy-main:hover,
136 .g1t-copy-more summary:hover {
137 background: var(--g1t-surface);
138 color: var(--g1t-fg);
139 }
140 .g1t-copy-menu {
141 position: absolute;
142 top: calc(100% + 0.4rem);
143 right: 0;
144 z-index: 10;
145 width: 16.5rem;
146 border-radius: 0.75rem;
147 padding: 0.35rem;
148 background: var(--g1t-raised);
149 box-shadow:
150 inset 0 0 0 1px var(--g1t-line-strong),
151 0 12px 32px rgb(0 0 0 / 0.45);
152 }
153 /* Its own spacing: the page's prose rules stop at not-content, and
154 nothing inside keeps a margin of its own. */
155 .g1t-copy-menu * {
156 margin: 0;
157 }
158 .g1t-copy-menu a {
159 display: flex;
160 align-items: flex-start;
161 gap: 0.6rem;
162 border-radius: 0.5rem;
163 padding: 0.5rem 0.6rem;
164 text-decoration: none;
165 transition: background 0.15s;
166 }
167 .g1t-copy-menu a + a {
168 margin-top: 0.1rem;
169 }
170 .g1t-copy-menu a:hover {
171 background: var(--g1t-surface);
172 }
173 .g1t-copy-menu :global(.g1t-copy-icon) {
174 flex-shrink: 0;
175 margin-top: 0.15rem;
176 color: var(--g1t-faint);
177 }
178 .g1t-copy-menu :global(.g1t-copy-out) {
179 flex-shrink: 0;
180 margin-left: auto;
181 margin-top: 0.15rem;
182 width: 0.8rem;
183 height: 0.8rem;
184 color: var(--g1t-faint);
185 opacity: 0;
186 transition: opacity 0.15s;
187 }
188 .g1t-copy-menu a:hover :global(.g1t-copy-out) {
189 opacity: 1;
190 }
191 .g1t-copy-text {
192 display: flex;
193 min-width: 0;
194 flex-direction: column;
195 gap: 0.1rem;
196 line-height: 1.3;
197 }
198 .g1t-copy-menu strong {
199 font-size: 0.8125rem;
200 font-weight: 500;
201 color: var(--g1t-fg);
202 }
203 .g1t-copy-menu small {
204 font-size: 0.75rem;
205 color: var(--g1t-faint);
206 }
207 @media (max-width: 40rem) {
208 .g1t-copy {
209 display: none;
210 }
211 }
212</style>