@local-civics/hub-ui 0.1.229 → 0.1.231

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/dist/index.d.ts CHANGED
@@ -1498,15 +1498,16 @@ type FileListProps = {
1498
1498
  onLessonClick?: (lessonId: string) => void;
1499
1499
  };
1500
1500
  /**
1501
- * The per-file list shown inside each of FileLocker's grouped sections. Which columns render
1502
- * depends on which level of the Badge -> Lesson -> Question hierarchy the calling group has
1503
- * already fixed (shown once at the group's own header instead - which is also where the
1504
- * pathway, the one level above Badge, always surfaces here, since every call site groups by
1505
- * pathway/badge/lesson and none renders an ungrouped flat list that would need a Pathway column
1506
- * of its own). Header and every row are direct children of one shared grid (not one grid per
1507
- * row) so column tracks are computed once across all content - a per-row grid would let each
1508
- * row's auto-sized action column resolve to a different width than the empty header cell above
1509
- * it, drifting the columns out of alignment. Preview/Download both point at the same existing
1501
+ * The per-file list shown inside each of FileLocker's grouped sections. Which columns render, and
1502
+ * in what order, follows the real Badge -> Lesson -> Question hierarchy left to right (pathway,
1503
+ * the one level above Badge, always surfaces at the group's own header instead - every call site
1504
+ * groups by pathway/badge/lesson and none renders an ungrouped flat list that would need a Pathway
1505
+ * column of its own). Whichever level the calling group has already fixed simply drops out of the
1506
+ * sequence, it never reorders what's left - so Question, the leaf, always sits immediately before
1507
+ * the action column, not first. Header and every row are direct children of one shared grid (not
1508
+ * one grid per row) so column tracks are computed once across all content - a per-row grid would
1509
+ * let each row's auto-sized action column resolve to a different width than the empty header cell
1510
+ * above it, drifting the columns out of alignment. Preview/Download both point at the same existing
1510
1511
  * `link` - there's no separate filename/size data available (the link is an opaque storage key,
1511
1512
  * not a real filename), so `question` doubles as the file's label.
1512
1513
  * @param props
package/dist/index.js CHANGED
@@ -7031,42 +7031,32 @@ const FileList = (props) => {
7031
7031
  return null;
7032
7032
  }
7033
7033
  const columns = [
7034
- "minmax(0,2fr)",
7035
- !hideLesson && "minmax(0,1.3fr)",
7036
7034
  !hideBadge && "minmax(0,1.3fr)",
7035
+ !hideLesson && "minmax(0,1.3fr)",
7036
+ "minmax(0,2fr)",
7037
7037
  "auto"
7038
7038
  ].filter(Boolean).join(" ");
7039
7039
  const headerCell = "border-b border-slate-100 pb-2 text-[10.5px] font-extrabold uppercase tracking-wide text-slate-400";
7040
7040
  const dataCell = "py-3";
7041
- return /* @__PURE__ */ React__namespace.createElement("div", { className: "grid items-center gap-x-3", style: { gridTemplateColumns: columns } }, /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Question"), !hideLesson && /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Lesson"), !hideBadge && /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Badge"), /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }), items.map((row, i) => {
7041
+ return /* @__PURE__ */ React__namespace.createElement("div", { className: "grid items-center gap-x-3", style: { gridTemplateColumns: columns } }, !hideBadge && /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Badge"), !hideLesson && /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Lesson"), /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }, "Question"), /* @__PURE__ */ React__namespace.createElement("div", { className: headerCell }), items.map((row, i) => {
7042
7042
  const rowBorder = i > 0 ? "border-t border-slate-100" : "";
7043
- return /* @__PURE__ */ React__namespace.createElement(React__namespace.Fragment, { key: i }, /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} text-xs font-semibold text-dark-blue-400` }, row.question), !hideLesson && (row.lessonId && onLessonClick ? /* @__PURE__ */ React__namespace.createElement(
7043
+ return /* @__PURE__ */ React__namespace.createElement(React__namespace.Fragment, { key: i }, !hideBadge && (row.badgeId && onBadgeClick ? /* @__PURE__ */ React__namespace.createElement(
7044
7044
  "button",
7045
7045
  {
7046
7046
  type: "button",
7047
- onClick: () => onLessonClick(row.lessonId),
7047
+ onClick: () => onBadgeClick(row.badgeId),
7048
7048
  className: `${dataCell} ${rowBorder} truncate text-left text-[11px] font-semibold text-sky-blue-400 hover:underline`
7049
7049
  },
7050
- row.lessonName
7051
- ) : /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} truncate text-[11px] text-slate-500` }, row.lessonName)), !hideBadge && (row.badgeId && onBadgeClick ? /* @__PURE__ */ React__namespace.createElement(
7050
+ row.badgeName
7051
+ ) : /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} truncate text-[11px] text-slate-500` }, row.badgeName)), !hideLesson && (row.lessonId && onLessonClick ? /* @__PURE__ */ React__namespace.createElement(
7052
7052
  "button",
7053
7053
  {
7054
7054
  type: "button",
7055
- onClick: () => onBadgeClick(row.badgeId),
7055
+ onClick: () => onLessonClick(row.lessonId),
7056
7056
  className: `${dataCell} ${rowBorder} truncate text-left text-[11px] font-semibold text-sky-blue-400 hover:underline`
7057
7057
  },
7058
- row.badgeName
7059
- ) : /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} truncate text-[11px] text-slate-500` }, row.badgeName)), /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} flex shrink-0 gap-2` }, /* @__PURE__ */ React__namespace.createElement(
7060
- "a",
7061
- {
7062
- href: row.link,
7063
- target: "_blank",
7064
- rel: "noopener noreferrer",
7065
- className: "flex items-center gap-1 rounded-md border border-slate-200 px-2.5 py-1.5 text-[11px] font-bold text-slate-600 no-underline hover:bg-slate-50"
7066
- },
7067
- /* @__PURE__ */ React__namespace.createElement(icons$1.IconExternalLink, { size: 12, stroke: 2 }),
7068
- "Preview"
7069
- ), /* @__PURE__ */ React__namespace.createElement(
7058
+ row.lessonName
7059
+ ) : /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} truncate text-[11px] text-slate-500` }, row.lessonName)), /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} text-xs font-semibold text-dark-blue-400` }, row.question), /* @__PURE__ */ React__namespace.createElement("div", { className: `${dataCell} ${rowBorder} flex shrink-0 gap-2` }, /* @__PURE__ */ React__namespace.createElement(
7070
7060
  "a",
7071
7061
  {
7072
7062
  href: row.link,
@@ -7096,9 +7086,18 @@ const TABS = [
7096
7086
  { value: "badges", label: "By badge" },
7097
7087
  { value: "lessons", label: "By lesson" }
7098
7088
  ];
7089
+ const SEARCH_PLACEHOLDER = "Search by pathway, badge, lesson, or question";
7099
7090
  const DAY_MS = 24 * 60 * 60 * 1e3;
7091
+ const matchesSearch = (item, term) => {
7092
+ var _a;
7093
+ if (!term)
7094
+ return true;
7095
+ return item.badgeName.toLowerCase().includes(term) || item.lessonName.toLowerCase().includes(term) || !!((_a = item.pathwayName) == null ? void 0 : _a.toLowerCase().includes(term)) || item.question.toLowerCase().includes(term);
7096
+ };
7100
7097
  const FileLocker = (props) => {
7101
7098
  const [tab, setTab] = React__namespace.useState("pathways");
7099
+ const [search, setSearch] = React__namespace.useState("");
7100
+ const searchLower = search.trim().toLowerCase();
7102
7101
  const filtered = useFilteredSubmissions(props.submissions);
7103
7102
  const files = props.submissions.length;
7104
7103
  const now = Date.now();
@@ -7106,9 +7105,24 @@ const FileLocker = (props) => {
7106
7105
  const startOfMonth = new Date(new Date().getFullYear(), new Date().getMonth(), 1).getTime();
7107
7106
  const thisWeek = props.submissions.filter((s) => s.updatedAt && new Date(s.updatedAt).getTime() >= weekAgo).length;
7108
7107
  const thisMonth = props.submissions.filter((s) => s.updatedAt && new Date(s.updatedAt).getTime() >= startOfMonth).length;
7109
- const pathwayGroups = props.pathways.map((p) => ({ pathway: p, items: filtered.byPathway(p.pathwayId, props.badges) })).filter((g) => g.items.length > 0);
7110
- const badgeGroups = props.badges.map((b) => ({ badge: b, items: filtered.byBadge(b.badgeId) })).filter((g) => g.items.length > 0);
7111
- const lessonGroups = props.lessons.map((l) => ({ lesson: l, items: filtered.byLesson(l.lessonId) })).filter((g) => g.items.length > 0);
7108
+ const pathwayGroups = props.pathways.map((p) => {
7109
+ const base = filtered.byPathway(p.pathwayId, props.badges);
7110
+ const ownNameMatches = !searchLower || p.title.toLowerCase().includes(searchLower);
7111
+ const items = ownNameMatches ? base : base.filter((it) => matchesSearch(it, searchLower));
7112
+ return { pathway: p, items };
7113
+ }).filter((g) => g.items.length > 0);
7114
+ const badgeGroups = props.badges.map((b) => {
7115
+ const base = filtered.byBadge(b.badgeId);
7116
+ const ownNameMatches = !searchLower || b.displayName.toLowerCase().includes(searchLower);
7117
+ const items = ownNameMatches ? base : base.filter((it) => matchesSearch(it, searchLower));
7118
+ return { badge: b, items };
7119
+ }).filter((g) => g.items.length > 0);
7120
+ const lessonGroups = props.lessons.map((l) => {
7121
+ const base = filtered.byLesson(l.lessonId);
7122
+ const ownNameMatches = !searchLower || l.lessonName.toLowerCase().includes(searchLower);
7123
+ const items = ownNameMatches ? base : base.filter((it) => matchesSearch(it, searchLower));
7124
+ return { lesson: l, items };
7125
+ }).filter((g) => g.items.length > 0);
7112
7126
  return /* @__PURE__ */ React__namespace.createElement("div", { className: "flex flex-col gap-5" }, /* @__PURE__ */ React__namespace.createElement("div", null, /* @__PURE__ */ React__namespace.createElement("h1", { className: "text-2xl font-extrabold tracking-tight text-dark-blue-400" }, props.displayName || "File Locker"), /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-500" }, props.description || "Files and links you've submitted through your lessons")), /* @__PURE__ */ React__namespace.createElement("div", { className: "flex gap-2.5" }, /* @__PURE__ */ React__namespace.createElement(StatCell, { icon: icons$1.IconClipboard, value: files, label: "Files", accent: "cyan" }), /* @__PURE__ */ React__namespace.createElement(StatCell, { icon: icons$1.IconCalendar, value: thisWeek, label: "This Week", accent: "mint" }), /* @__PURE__ */ React__namespace.createElement(StatCell, { icon: icons$1.IconCalendarStats, value: thisMonth, label: "This Month", accent: "gold" })), /* @__PURE__ */ React__namespace.createElement("div", { className: "flex w-fit gap-1 rounded-xl border border-slate-200 bg-white p-1" }, TABS.map((t) => /* @__PURE__ */ React__namespace.createElement(
7113
7127
  "button",
7114
7128
  {
@@ -7117,7 +7131,16 @@ const FileLocker = (props) => {
7117
7131
  className: `rounded-lg px-4 py-2 text-xs font-bold ${tab === t.value ? "bg-sky-blue-400/20 text-dark-blue-400" : "text-slate-400 hover:text-slate-600"}`
7118
7132
  },
7119
7133
  t.label
7120
- ))), /* @__PURE__ */ React__namespace.createElement("div", { className: "flex flex-col gap-3" }, props.loading && /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-400" }, "Loading..."), !props.loading && tab === "pathways" && pathwayGroups.map(({ pathway, items }) => /* @__PURE__ */ React__namespace.createElement(
7134
+ ))), /* @__PURE__ */ React__namespace.createElement("div", { className: "flex items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-4 py-3 shadow-sm" }, /* @__PURE__ */ React__namespace.createElement(icons$1.IconSearch, { size: 15, stroke: 1.75, className: "text-slate-400" }), /* @__PURE__ */ React__namespace.createElement(
7135
+ "input",
7136
+ {
7137
+ type: "text",
7138
+ value: search,
7139
+ onChange: (e) => setSearch(e.target.value),
7140
+ placeholder: SEARCH_PLACEHOLDER,
7141
+ className: "flex-1 border-none bg-transparent text-sm text-dark-blue-400 outline-none placeholder:text-slate-400"
7142
+ }
7143
+ )), /* @__PURE__ */ React__namespace.createElement("div", { className: "flex flex-col gap-3" }, props.loading && /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-400" }, "Loading..."), !props.loading && tab === "pathways" && pathwayGroups.map(({ pathway, items }) => /* @__PURE__ */ React__namespace.createElement(
7121
7144
  GroupCard,
7122
7145
  {
7123
7146
  key: pathway.pathwayId,
@@ -7126,7 +7149,25 @@ const FileLocker = (props) => {
7126
7149
  onClick: props.onPathwayClick ? () => props.onPathwayClick(pathway.pathwayId) : void 0
7127
7150
  },
7128
7151
  /* @__PURE__ */ React__namespace.createElement(FileList, { items, onBadgeClick: props.onBadgeClick, onLessonClick: props.onLessonClick })
7129
- )), !props.loading && tab === "badges" && badgeGroups.map(({ badge, items }) => /* @__PURE__ */ React__namespace.createElement(GroupCard, { key: badge.badgeId, title: badge.displayName, description: badgeSubtitle(items) }, /* @__PURE__ */ React__namespace.createElement(FileList, { items, hideBadge: true, onLessonClick: props.onLessonClick }))), !props.loading && tab === "lessons" && lessonGroups.map(({ lesson, items }) => /* @__PURE__ */ React__namespace.createElement(GroupCard, { key: lesson.lessonId, title: lesson.lessonName, description: lessonSubtitle(items) }, /* @__PURE__ */ React__namespace.createElement(FileList, { items, hideBadge: true, hideLesson: true }))), !props.loading && files === 0 && /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-400" }, "No files to display.")));
7152
+ )), !props.loading && tab === "badges" && badgeGroups.map(({ badge, items }) => /* @__PURE__ */ React__namespace.createElement(
7153
+ GroupCard,
7154
+ {
7155
+ key: badge.badgeId,
7156
+ title: badge.displayName,
7157
+ description: badgeSubtitle(items),
7158
+ onClick: props.onBadgeClick ? () => props.onBadgeClick(badge.badgeId) : void 0
7159
+ },
7160
+ /* @__PURE__ */ React__namespace.createElement(FileList, { items, hideBadge: true, onLessonClick: props.onLessonClick })
7161
+ )), !props.loading && tab === "lessons" && lessonGroups.map(({ lesson, items }) => /* @__PURE__ */ React__namespace.createElement(
7162
+ GroupCard,
7163
+ {
7164
+ key: lesson.lessonId,
7165
+ title: lesson.lessonName,
7166
+ description: lessonSubtitle(items),
7167
+ onClick: props.onLessonClick ? () => props.onLessonClick(lesson.lessonId) : void 0
7168
+ },
7169
+ /* @__PURE__ */ React__namespace.createElement(FileList, { items, hideBadge: true, hideLesson: true })
7170
+ )), !props.loading && files > 0 && searchLower && (tab === "pathways" && pathwayGroups.length === 0 || tab === "badges" && badgeGroups.length === 0 || tab === "lessons" && lessonGroups.length === 0) && /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-400" }, "No matching entries."), !props.loading && files === 0 && /* @__PURE__ */ React__namespace.createElement("p", { className: "text-sm text-slate-400" }, "No files to display.")));
7130
7171
  };
7131
7172
  const badgeSubtitle = (items) => {
7132
7173
  var _a;