@openbooknetwork/kitap 0.1.0 → 0.3.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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/app.html +11 -4
- package/src/app.js +16 -2
- package/src/build.mjs +2 -2
- package/src/dev.mjs +2 -2
- package/src/i18n/en.yaml +4 -0
- package/src/i18n/tr.yaml +4 -0
- package/src/schema.mjs +1 -0
package/README.md
CHANGED
|
@@ -26,6 +26,7 @@ title: Open Body Book
|
|
|
26
26
|
engine: beden
|
|
27
27
|
languages: [en, tr]
|
|
28
28
|
assets: { url: https://assets.example.org/, bucket: body-assets, endpoint: https://... }
|
|
29
|
+
ask: [chatgpt, claude] # optional: an Ask button on every page opens the assistant with the page's text
|
|
29
30
|
```
|
|
30
31
|
|
|
31
32
|
```yaml
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openbooknetwork/kitap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "The book under the Open Book Network sites: stories of pages with a text beside a picture an engine draws, built to a static site in every language",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"static-site",
|
package/src/app.html
CHANGED
|
@@ -111,7 +111,11 @@ nav.pages button:focus-visible,nav.pages a.pg:focus-visible{outline:2px solid va
|
|
|
111
111
|
.legend .r{width:1rem;height:0;border:0;border-top:2px solid var(--rc);position:relative}
|
|
112
112
|
.legend .r.arrow::after{content:"";position:absolute;inset-inline-end:-2px;top:-5px;border:4px solid transparent;border-inline-start:5px solid var(--rc)}
|
|
113
113
|
.legend .r.dashed{border-top-style:dashed}
|
|
114
|
-
|
|
114
|
+
/* the corner of the map: the Ask button over the credit bar, and the popups they open above both */
|
|
115
|
+
.corner{position:absolute;inset-inline-end:.714rem;bottom:.714rem;z-index:4;display:flex;flex-direction:column;align-items:flex-end;gap:.429rem;pointer-events:auto}
|
|
116
|
+
.ask{display:flex;align-items:center;gap:.357rem;background:var(--panel);border:1px solid var(--rule);border-radius:6px;padding:.357rem .714rem;font:inherit;font-size:.929rem;font-weight:600;color:var(--ink);cursor:pointer}
|
|
117
|
+
.ask svg{width:1.143rem;height:1.143rem} .ask[hidden]{display:none} .ask:focus-visible{outline:2px solid var(--accent)}
|
|
118
|
+
.credit{background:var(--panel);border:1px solid var(--rule);padding:3px .571rem;font-size:.786rem;color:var(--ink-2);pointer-events:auto;
|
|
115
119
|
display:flex;align-items:center;gap:.571rem}
|
|
116
120
|
.credit a{color:inherit}
|
|
117
121
|
.credit :is(#mapdata,#othersrc){background:none;border:0;border-inline-start:1px solid var(--rule);padding:0;padding-inline-start:.571rem;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
|
|
@@ -124,6 +128,7 @@ nav.pages button:focus-visible,nav.pages a.pg:focus-visible{outline:2px solid va
|
|
|
124
128
|
.credits ul{margin:0;padding-inline-start:1rem;display:grid;gap:.286rem}
|
|
125
129
|
.credit #othersrc[hidden]{display:none}
|
|
126
130
|
.credits .made{display:none;margin:0 0 .429rem}
|
|
131
|
+
.credits .ask-list{display:grid;gap:.286rem;margin:0 0 .429rem} .credits .note{margin:0;color:var(--ink-2)}
|
|
127
132
|
/* map tools: the 2D | 3D switch at the top of the map, joined on phones by the Legend button */
|
|
128
133
|
.maptools{position:absolute;top:.714rem;inset-inline-end:.714rem;z-index:3;display:flex;gap:.429rem;align-items:flex-start}
|
|
129
134
|
.view{display:flex;background:var(--panel);border:1px solid var(--rule);border-radius:6px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
|
@@ -253,7 +258,7 @@ aside.story figcaption small{display:block;font-size:.786rem;color:var(--ink-2);
|
|
|
253
258
|
aside.story .text{grid-row:2;grid-column:1;z-index:2;background:var(--paper);overflow:auto;padding:.571rem .143rem 1.143rem}
|
|
254
259
|
body[data-tab="map"] aside.story .text{display:none}
|
|
255
260
|
/* in the text view the map's own overlays step aside; they sit above the text in the stacking order otherwise */
|
|
256
|
-
body[data-tab="text"] :is(.peek,.maptools,.legend,.
|
|
261
|
+
body[data-tab="text"] :is(.peek,.maptools,.legend,.corner,.maplibregl-popup,.hl-label){display:none}
|
|
257
262
|
.hl-label.pinned{top:calc(.571rem + max(40px,2.857rem) + .429rem)} /* below the map tools and the credit */
|
|
258
263
|
/* the peek card: the first two lines of the page's text over the bottom of the map; a tap opens the text */
|
|
259
264
|
.peek{display:block;position:absolute;inset-inline:.571rem;bottom:.571rem;z-index:3;background:var(--panel);border:1px solid var(--rule);border-radius:8px;
|
|
@@ -272,7 +277,9 @@ aside.story figcaption small{display:block;font-size:.786rem;color:var(--ink-2);
|
|
|
272
277
|
.legend-btn[hidden]{display:none}
|
|
273
278
|
.legend{top:calc(.571rem + max(40px,2.857rem) + .429rem);bottom:auto;inset-inline-start:.571rem;z-index:3;max-width:calc(100% - 1.143rem)}
|
|
274
279
|
body:not(.legend-open) .legend{display:none}
|
|
275
|
-
.
|
|
280
|
+
.corner{top:.571rem;bottom:auto;inset-inline-end:.571rem;flex-direction:row-reverse}
|
|
281
|
+
.ask{display:grid;place-items:center;width:max(44px,3.143rem);height:max(40px,2.857rem);padding:0;color:var(--ink-2)} .ask span{display:none}
|
|
282
|
+
.credit{padding:0;border:0;background:none}
|
|
276
283
|
.credit #made{display:none}
|
|
277
284
|
.credit :is(#mapdata,#othersrc){display:grid;place-items:center;width:max(44px,3.143rem);height:max(40px,2.857rem);padding:0;background:var(--panel);border:1px solid var(--rule);border-radius:6px;color:var(--ink-2)}
|
|
278
285
|
.credit :is(#mapdata,#othersrc) svg{display:block} .credit :is(#mapdata,#othersrc) span{display:none}
|
|
@@ -338,7 +345,7 @@ aside.story figcaption small{display:block;font-size:.786rem;color:var(--ink-2);
|
|
|
338
345
|
<div class="maptools"><button type="button" class="legend-btn" id="legend-btn"></button><div class="view" id="view" role="group"><button type="button" data-view="2d" aria-pressed="true"></button><button type="button" data-view="3d" aria-pressed="false"></button></div></div>
|
|
339
346
|
<div class="legend" id="legend"></div>
|
|
340
347
|
<div class="hl-label" id="hl-label" hidden></div>
|
|
341
|
-
<div class="credit" id="credit"><span id="made"></span><button type="button" id="mapdata" aria-expanded="false" aria-controls="credits"></button><div class="credits" id="credits" hidden></div><button type="button" id="othersrc" aria-expanded="false" aria-controls="othercredits" hidden></button><div class="credits" id="othercredits" hidden></div></div>
|
|
348
|
+
<div class="corner" id="corner"><button type="button" class="ask" id="ask" aria-expanded="false" aria-controls="askmenu" hidden></button><div class="credits" id="askmenu" hidden></div><div class="credit" id="credit"><span id="made"></span><button type="button" id="mapdata" aria-expanded="false" aria-controls="credits"></button><div class="credits" id="credits" hidden></div><button type="button" id="othersrc" aria-expanded="false" aria-controls="othercredits" hidden></button><div class="credits" id="othercredits" hidden></div></div></div>
|
|
342
349
|
<div class="peek" id="peek" role="button" tabindex="0"><p id="peek-text"></p><span class="more" id="peek-more"></span><button type="button" class="close" id="peek-close">×</button></div>
|
|
343
350
|
<div class="lightbox" id="lightbox" hidden>
|
|
344
351
|
<figure><img id="lb-img" alt=""><figcaption id="lb-cap"></figcaption></figure>
|
package/src/app.js
CHANGED
|
@@ -63,6 +63,7 @@ function renderChrome(){
|
|
|
63
63
|
$('mapdata').innerHTML = `${ICONS.info}<span>${esc(T().map_data)}</span>`; $('mapdata').setAttribute('aria-label', T().map_data);
|
|
64
64
|
$('mapdata').hidden = !B.credits.length;
|
|
65
65
|
$('othersrc').innerHTML = `${ICONS['external-link']}<span>${esc(T().other_sources)}</span>`; $('othersrc').setAttribute('aria-label', T().other_sources);
|
|
66
|
+
$('ask').innerHTML = `${ICONS['message-circle']}<span>${esc(T().ask)}</span>`; $('ask').setAttribute('aria-label', T().ask); $('ask').hidden = !B.site.ask.length;
|
|
66
67
|
$('credits').innerHTML = `<p class="made">${credit(T())}</p><h4>${esc(T().map_data)}</h4><ul>${B.credits.map(c => `<li>${esc(L(c))}</li>`).join('')}</ul>`;
|
|
67
68
|
$('prev').innerHTML = `${ICONS['chevron-left']} ${esc(T().prev)}`;
|
|
68
69
|
$('next').innerHTML = `${esc(T().next)} ${ICONS['chevron-right']}`;
|
|
@@ -201,11 +202,24 @@ $('peek').onkeydown = e => { if (e.key === 'Enter' || e.key === ' ') { e.prevent
|
|
|
201
202
|
$('peek-close').onclick = e => { e.stopPropagation(); $('peek').hidden = true; };
|
|
202
203
|
$('legend-btn').onclick = () => document.body.classList.toggle('legend-open');
|
|
203
204
|
// the data credit popup and the page's other sources popup share the credit bar, one open at a time
|
|
204
|
-
const CREDIT_POPUPS = { mapdata: 'credits', othersrc: 'othercredits' };
|
|
205
|
+
const CREDIT_POPUPS = { mapdata: 'credits', othersrc: 'othercredits', ask: 'askmenu' };
|
|
205
206
|
const showCredits = (open, btn = 'mapdata') => { for (const [b, p] of Object.entries(CREDIT_POPUPS)) { const on = open && b === btn; $(p).hidden = !on; $(b).setAttribute('aria-expanded', on); } };
|
|
206
207
|
$('mapdata').onclick = () => showCredits($('credits').hidden, 'mapdata');
|
|
207
208
|
$('othersrc').onclick = () => showCredits($('othercredits').hidden, 'othersrc');
|
|
208
|
-
|
|
209
|
+
// the assistants of site.yaml, each opened in a new tab with the page's text as the prompt; the reader's own account answers
|
|
210
|
+
const ASSISTANTS = { chatgpt: { name: 'ChatGPT', url: q => `https://chatgpt.com/?q=${q}` }, claude: { name: 'Claude', url: q => `https://claude.ai/new?q=${q}` } };
|
|
211
|
+
function askPrompt(p){
|
|
212
|
+
const d = document.createElement('div'); d.innerHTML = p === OVERVIEW ? `<p>${esc(L(B.summary))}</p>` : p.html[lang];
|
|
213
|
+
d.querySelectorAll('figure').forEach(f => f.remove()); // the prose only, without the image captions and credits
|
|
214
|
+
const text = d.textContent.replace(/\s+/g, ' ').trim().slice(0, 2500);
|
|
215
|
+
return `${T().ask_prompt.replace('{title}', L(p.title)).replace('{url}', location.href)}\n\n${text}`;
|
|
216
|
+
}
|
|
217
|
+
$('ask').onclick = () => {
|
|
218
|
+
const q = encodeURIComponent(askPrompt(here()));
|
|
219
|
+
$('askmenu').innerHTML = `<h4>${esc(T().ask_title)}</h4><div class="ask-list">${B.site.ask.map(id => `<a href="${esc(ASSISTANTS[id].url(q))}" target="_blank" rel="noopener">${esc(ASSISTANTS[id].name)} ${ICONS['external-link']}</a>`).join('')}</div><p class="note">${esc(T().ask_note)}</p>`;
|
|
220
|
+
showCredits($('askmenu').hidden, 'ask');
|
|
221
|
+
};
|
|
222
|
+
document.addEventListener('click', e => { if (!e.target.closest('.corner')) showCredits(false); });
|
|
209
223
|
const showSettings = open => { $('settings').hidden = !open; $('settings-btn').setAttribute('aria-expanded', open); };
|
|
210
224
|
$('settings-btn').onclick = () => showSettings($('settings').hidden);
|
|
211
225
|
document.addEventListener('click', e => { if (!$('settings').hidden && !e.target.closest('.prefs')) showSettings(false); });
|
package/src/build.mjs
CHANGED
|
@@ -48,7 +48,7 @@ const readUi = dir => Object.fromEntries(fs.readdirSync(dir).filter(f => f.endsW
|
|
|
48
48
|
const UI = readUi(UI_DIR);
|
|
49
49
|
// Icons come from Lucide: the shell's own, and the ones an engine asks for by name
|
|
50
50
|
const ICON_DIR = path.dirname(require.resolve('lucide-static/icons/flag.svg'));
|
|
51
|
-
const SHELL_ICONS = ['x', 'external-link', 'chevron-left', 'chevron-right', 'list', 'info', 'settings', 'languages'];
|
|
51
|
+
const SHELL_ICONS = ['x', 'external-link', 'chevron-left', 'chevron-right', 'list', 'info', 'settings', 'languages', 'message-circle'];
|
|
52
52
|
function iconSvg(name, where) {
|
|
53
53
|
const file = path.join(ICON_DIR, name + '.svg');
|
|
54
54
|
if (!exists(file)) fail(where, `unknown icon "${name}", pick a name from https://lucide.dev/icons`);
|
|
@@ -375,7 +375,7 @@ function buildStory(p, storyDir, site, engine) {
|
|
|
375
375
|
// --- bundle and pages ---
|
|
376
376
|
const bundle = {
|
|
377
377
|
id: story.id, title: T(story.title, 'story.title'), summary, span, cover, languages: langs, defaultLanguage: defaultLang, ui, families, engine: engine.id,
|
|
378
|
-
site: { title: siteT(site.title, 'site.title'), languages: site.langs, defaultLanguage: site.defaultLang,
|
|
378
|
+
site: { title: siteT(site.title, 'site.title'), languages: site.langs, defaultLanguage: site.defaultLang, ask: site.ask ?? [],
|
|
379
379
|
source: site.repository ? `${site.repository.replace(/\/$/, '')}/tree/${site.branch}/${where.split(path.sep).join('/')}` : null },
|
|
380
380
|
themes: themeList(site.themes, ui, langs), defaultTheme: story.theme ?? site.theme,
|
|
381
381
|
credits: eb.credits ?? [], images, icons, pages: pages.map(({ dir, include, ...p }) => p), tree: navTree(tree),
|
package/src/dev.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const RELOAD = `<script>(()=>{let v=null;setInterval(async()=>{try{const r=await
|
|
|
12
12
|
// Serve root/dist on the port and rebuild on changes: a change in one story's folder rebuilds that story and the main
|
|
13
13
|
// page, any other change every story. Returns the http server. Builds run in a child process, so package edits apply,
|
|
14
14
|
// and with --local-assets, so the images on this machine show before they are uploaded.
|
|
15
|
-
// story serves that one story as a site of its own
|
|
15
|
+
// story serves that one story as a site of its own. Every build writes into out, by default dist/, which the server serves.
|
|
16
16
|
export function dev({ root = process.cwd(), port = Number(process.env.PORT) || 8080, log = console.log, story = null, out = null, engine = null } = {}) {
|
|
17
17
|
const DIST = out ? path.resolve(root, out) : path.join(root, 'dist'), CONTENT = path.join(root, 'content');
|
|
18
18
|
// engine is the engine module's path, handed to each build and watched beside kitap's own source
|
|
@@ -25,7 +25,7 @@ export function dev({ root = process.cwd(), port = Number(process.env.PORT) || 8
|
|
|
25
25
|
building = true;
|
|
26
26
|
const t0 = Date.now(), wasAll = all, stories = [...changed];
|
|
27
27
|
all = false; changed = new Set();
|
|
28
|
-
const scope = story ? ['--story', story
|
|
28
|
+
const scope = [...(story ? ['--story', story] : wasAll ? [] : stories.flatMap(s => ['--only', s])), ...(out ? ['--out', out] : [])];
|
|
29
29
|
const p = spawn(process.execPath, [path.join(PKG, 'bin', 'kitap.mjs'), 'build', '--local-assets', ...(engine ? ['--engine', engine] : []), ...scope], { stdio: 'inherit', cwd: root });
|
|
30
30
|
p.on('exit', code => {
|
|
31
31
|
building = false;
|
package/src/i18n/en.yaml
CHANGED
|
@@ -28,6 +28,10 @@ search_stories: Search stories
|
|
|
28
28
|
no_match: "No story matches “{q}”."
|
|
29
29
|
map_data: Data
|
|
30
30
|
other_sources: Other sources
|
|
31
|
+
ask: Ask AI
|
|
32
|
+
ask_title: Ask about this page
|
|
33
|
+
ask_prompt: "I am reading “{title}” at {url}. Answer my questions about it from this text and the page's sources, and say when you are unsure."
|
|
34
|
+
ask_note: Opens in a new tab with the page's text as the question.
|
|
31
35
|
start: Start
|
|
32
36
|
not_found: There is no page at this address.
|
|
33
37
|
settings: Settings
|
package/src/i18n/tr.yaml
CHANGED
|
@@ -28,6 +28,10 @@ search_stories: Hikâye ara
|
|
|
28
28
|
no_match: "“{q}” ile eşleşen hikâye yok."
|
|
29
29
|
map_data: Veri
|
|
30
30
|
other_sources: Diğer kaynaklar
|
|
31
|
+
ask: Yapay zekâya sor
|
|
32
|
+
ask_title: Bu sayfayı sor
|
|
33
|
+
ask_prompt: "“{title}” sayfasını okuyorum: {url}. Sorularımı bu metne ve sayfanın kaynaklarına dayanarak yanıtla, emin olmadığında söyle."
|
|
34
|
+
ask_note: Yeni sekmede, sayfanın metni soru olarak açılır.
|
|
31
35
|
start: Başla
|
|
32
36
|
not_found: Bu adreste bir sayfa yok.
|
|
33
37
|
settings: Ayarlar
|
package/src/schema.mjs
CHANGED
|
@@ -28,6 +28,7 @@ export const Site = z.object({
|
|
|
28
28
|
themes: Themes.optional(),
|
|
29
29
|
url: z.string().url().optional(), // where the site is published, for canonical links, sitemaps and llms.txt
|
|
30
30
|
repository: z.string().url().optional(), // the content repository: a Contribute link on the main page, a source link per story
|
|
31
|
+
ask: z.array(z.enum(['chatgpt', 'claude'])).default([]), // assistants a page offers to open with its text as the prompt
|
|
31
32
|
branch: z.string().default('main'),
|
|
32
33
|
// the object store that holds the images and the engine's objects: url serves them, bucket and endpoint take uploads
|
|
33
34
|
assets: z.object({ url: z.string().url().refine(u => u.endsWith('/'), 'ends in /'), bucket: z.string().min(1), endpoint: z.string().url() }).strict().optional(),
|