education-playlists 0.1.35 → 0.1.37

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 CHANGED
@@ -22,6 +22,9 @@ the answers into searches and the searches into a playlist.
22
22
  community list or AI search, and whether the URL has been verified.
23
23
  - **Quiz hook.** Pass a `quizGenerator` (for example NotebookLM over the item's
24
24
  material) and each item gets a "Quiz me" button.
25
+ - **Open items in the host.** Pass `onOpenItem` and clicking an item calls it
26
+ instead of opening the page; a small ↗ beside the title still opens the page
27
+ in a new tab. qwksearch uses this to read the page and ask about it.
25
28
 
26
29
  The bundled catalog is a hand-curated seed of 28 MIT OpenCourseWare courses in
27
30
  11 playlists. Course pages link to their canonical `ocw.mit.edu/courses/…`
@@ -8,7 +8,7 @@
8
8
  * `compact` it is the full-page view.
9
9
  */
10
10
  import React from 'react';
11
- import type { EducationCatalog } from '../types';
11
+ import type { EducationCatalog, PlaylistItem } from '../types';
12
12
  import type { QuizGenerator } from '../quiz';
13
13
  export interface EducationPlaylistsProps {
14
14
  className?: string;
@@ -40,5 +40,12 @@ export interface EducationPlaylistsProps {
40
40
  playlistsEndpoint?: string;
41
41
  /** Turns on "Quiz me" for each item (NotebookLM or another quiz source). */
42
42
  quizGenerator?: QuizGenerator;
43
+ /**
44
+ * Called when the learner clicks an item, instead of opening its page. The
45
+ * host can read the page and start a conversation about it (qwksearch opens
46
+ * its article reader, which extracts the page's text and asks about it).
47
+ * Without it, items are plain links that open in a new tab.
48
+ */
49
+ onOpenItem?: (item: PlaylistItem) => void;
43
50
  }
44
- export declare function EducationPlaylists({ className, style, compact, catalog: catalogProp, planEndpoint, openHref, shareBaseHref, importFromHash, userId, playlistsEndpoint, quizGenerator, }: EducationPlaylistsProps): React.JSX.Element;
51
+ export declare function EducationPlaylists({ className, style, compact, catalog: catalogProp, planEndpoint, openHref, shareBaseHref, importFromHash, userId, playlistsEndpoint, quizGenerator, onOpenItem, }: EducationPlaylistsProps): React.JSX.Element;
@@ -4,7 +4,7 @@
4
4
  * sharing controls.
5
5
  */
6
6
  import React from 'react';
7
- import type { Playlist } from '../types';
7
+ import type { Playlist, PlaylistItem } from '../types';
8
8
  import type { QuizGenerator } from '../quiz';
9
9
  export interface PlaylistViewProps {
10
10
  playlist: Playlist;
@@ -23,5 +23,7 @@ export interface PlaylistViewProps {
23
23
  /** Saved to a server-side account, so invites reach the people invited. */
24
24
  inAccount?: boolean;
25
25
  quizGenerator?: QuizGenerator;
26
+ /** Called instead of following an item's link; see `EducationPlaylistsProps.onOpenItem`. */
27
+ onOpenItem?: (item: PlaylistItem) => void;
26
28
  }
27
- export declare function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, canShare, inAccount, quizGenerator }: PlaylistViewProps): React.JSX.Element;
29
+ export declare function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, canShare, inAccount, quizGenerator, onOpenItem }: PlaylistViewProps): React.JSX.Element;
@@ -25,6 +25,7 @@ export declare const s: {
25
25
  readonly list: CSSProperties;
26
26
  readonly item: CSSProperties;
27
27
  readonly link: CSSProperties;
28
+ readonly textButton: CSSProperties;
28
29
  readonly badge: CSSProperties;
29
30
  readonly section: CSSProperties;
30
31
  };
package/dist/index.cjs CHANGED
@@ -944,6 +944,7 @@ var s = {
944
944
  list: { listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 4 },
945
945
  item: { display: "flex", alignItems: "flex-start", gap: 8, padding: "6px 4px", borderBottom: "1px solid rgba(127,127,127,0.15)" },
946
946
  link: { color: "inherit", textDecoration: "none", fontWeight: 500 },
947
+ textButton: { border: "none", background: "transparent", padding: 0, font: "inherit", textAlign: "left", cursor: "pointer", minWidth: 0 },
947
948
  badge: { fontSize: 10, border: line, borderRadius: 4, padding: "0 5px", opacity: 0.8, whiteSpace: "nowrap" },
948
949
  section: { display: "flex", flexDirection: "column", gap: 6 }
949
950
  };
@@ -981,7 +982,8 @@ function ItemRow({
981
982
  checked,
982
983
  onToggle,
983
984
  onRemove,
984
- quizGenerator
985
+ quizGenerator,
986
+ onOpenItem
985
987
  }) {
986
988
  const [quiz, setQuiz] = (0, import_react.useState)(null);
987
989
  const [quizState, setQuizState] = (0, import_react.useState)("idle");
@@ -995,6 +997,7 @@ function ItemRow({
995
997
  setQuizState("error");
996
998
  }
997
999
  };
1000
+ const titleStyle = { ...s.link, textDecoration: checked ? "line-through" : "none", opacity: checked ? 0.6 : 1 };
998
1001
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", { style: s.item, children: [
999
1002
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1000
1003
  "input",
@@ -1007,16 +1010,23 @@ function ItemRow({
1007
1010
  }
1008
1011
  ),
1009
1012
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 2 }, children: [
1010
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1011
- "a",
1012
- {
1013
- href: item.url,
1014
- target: "_blank",
1015
- rel: "noopener noreferrer",
1016
- style: { ...s.link, textDecoration: checked ? "line-through" : "none", opacity: checked ? 0.6 : 1 },
1017
- children: item.title
1018
- }
1019
- ),
1013
+ onOpenItem ? (
1014
+ // The host reads the page and asks about it; the page itself is
1015
+ // still one click away on the arrow beside the title.
1016
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: 6, minWidth: 0 }, children: [
1017
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1018
+ "button",
1019
+ {
1020
+ type: "button",
1021
+ onClick: () => onOpenItem(item),
1022
+ title: "Read this page and ask questions about it",
1023
+ style: { ...titleStyle, ...s.textButton },
1024
+ children: item.title
1025
+ }
1026
+ ),
1027
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: item.url, target: "_blank", rel: "noopener noreferrer", style: { ...s.link, ...s.muted }, "aria-label": `Open "${item.title}" in a new tab`, title: "Open the page", children: "\u2197" })
1028
+ ] })
1029
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", { href: item.url, target: "_blank", rel: "noopener noreferrer", style: titleStyle, children: item.title }),
1020
1030
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { ...s.row, ...s.muted }, children: [
1021
1031
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: s.badge, children: KIND_LABEL[item.kind] }),
1022
1032
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { title: item.estimateBasis, children: [
@@ -1106,7 +1116,7 @@ function SharingControls({ playlist, onChange, shareBaseHref, inAccount }) {
1106
1116
  message && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { ...s.muted, wordBreak: "break-all" }, children: message })
1107
1117
  ] });
1108
1118
  }
1109
- function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, canShare = true, inAccount = false, quizGenerator }) {
1119
+ function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesPerWeek, shareBaseHref, canShare = true, inAccount = false, quizGenerator, onOpenItem }) {
1110
1120
  const time = playlistTime(playlist, done);
1111
1121
  const weeks = weeksToFinish(time.remainingMinutes, minutesPerWeek);
1112
1122
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: s.section, children: [
@@ -1143,7 +1153,8 @@ function PlaylistView({ playlist, done, onToggle, onChange, onSaveCopy, minutesP
1143
1153
  checked: done.has(item.id),
1144
1154
  onToggle: () => onToggle(item.id),
1145
1155
  onRemove: onChange ? () => onChange(removeItem(playlist, item.id)) : void 0,
1146
- quizGenerator
1156
+ quizGenerator,
1157
+ onOpenItem
1147
1158
  },
1148
1159
  item.id
1149
1160
  )) }),
@@ -1282,7 +1293,8 @@ function EducationPlaylists({
1282
1293
  importFromHash = false,
1283
1294
  userId,
1284
1295
  playlistsEndpoint,
1285
- quizGenerator
1296
+ quizGenerator,
1297
+ onOpenItem
1286
1298
  }) {
1287
1299
  const catalog = (0, import_react3.useMemo)(() => catalogProp ?? getDefaultCatalog(), [catalogProp]);
1288
1300
  const progressStore = (0, import_react3.useMemo)(() => createProgressStore(), []);
@@ -1466,7 +1478,8 @@ function EducationPlaylists({
1466
1478
  inAccount: Boolean(remote),
1467
1479
  minutesPerWeek: open.plan?.minutesPerWeek,
1468
1480
  shareBaseHref: shareBase,
1469
- quizGenerator
1481
+ quizGenerator,
1482
+ onOpenItem
1470
1483
  }
1471
1484
  )
1472
1485
  ] });