Compare commits
No commits in common. "533783bcae3dff6581d54a1f28a42164c7e3afb8" and "b061e2ce611ea489b6b93fb63cad4c4e39cdfa65" have entirely different histories.
533783bcae
...
b061e2ce61
@ -9,10 +9,6 @@
|
|||||||
"CML_SERVICE_EMAIL": "admin@bnkops.ca",
|
"CML_SERVICE_EMAIL": "admin@bnkops.ca",
|
||||||
"CML_SERVICE_PASSWORD": "ChangeMe2025!"
|
"CML_SERVICE_PASSWORD": "ChangeMe2025!"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"playwright": {
|
|
||||||
"command": "npx",
|
|
||||||
"args": ["@playwright/mcp@latest", "--headless"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -375,10 +375,10 @@ export default function App() {
|
|||||||
<Route path="/volunteer/challenges" element={<ChallengesPage />} />
|
<Route path="/volunteer/challenges" element={<ChallengesPage />} />
|
||||||
<Route path="/volunteer/challenges/:id" element={<ChallengeDetailPage />} />
|
<Route path="/volunteer/challenges/:id" element={<ChallengeDetailPage />} />
|
||||||
<Route path="/volunteer/tickets" element={<MyTicketsPage />} />
|
<Route path="/volunteer/tickets" element={<MyTicketsPage />} />
|
||||||
<Route path="/volunteer/calendar/shared/:id" element={<FeatureGate feature="enableSocialCalendar"><SharedCalendarViewPage /></FeatureGate>} />
|
<Route path="/volunteer/calendar/shared/:id" element={<SharedCalendarViewPage />} />
|
||||||
<Route path="/volunteer/calendar/shared" element={<FeatureGate feature="enableSocialCalendar"><SharedCalendarsPage /></FeatureGate>} />
|
<Route path="/volunteer/calendar/shared" element={<SharedCalendarsPage />} />
|
||||||
<Route path="/volunteer/calendar/friend/:userId" element={<FeatureGate feature="enableSocialCalendar"><FriendCalendarPage /></FeatureGate>} />
|
<Route path="/volunteer/calendar/friend/:userId" element={<FriendCalendarPage />} />
|
||||||
<Route path="/volunteer/calendar" element={<FeatureGate feature="enableSocialCalendar"><MyCalendarPage /></FeatureGate>} />
|
<Route path="/volunteer/calendar" element={<MyCalendarPage />} />
|
||||||
<Route path="/volunteer/*" element={<NotFoundPage />} />
|
<Route path="/volunteer/*" element={<NotFoundPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
@ -807,9 +807,7 @@ export default function App() {
|
|||||||
path="scheduling/calendar-views/:id"
|
path="scheduling/calendar-views/:id"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute requiredRoles={SCHEDULING_ROLES}>
|
<ProtectedRoute requiredRoles={SCHEDULING_ROLES}>
|
||||||
<FeatureGate feature="enableSocialCalendar">
|
<AdminCalendarViewPage />
|
||||||
<AdminCalendarViewPage />
|
|
||||||
</FeatureGate>
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -817,9 +815,7 @@ export default function App() {
|
|||||||
path="scheduling/calendar"
|
path="scheduling/calendar"
|
||||||
element={
|
element={
|
||||||
<ProtectedRoute requiredRoles={SCHEDULING_ROLES}>
|
<ProtectedRoute requiredRoles={SCHEDULING_ROLES}>
|
||||||
<FeatureGate feature="enableSocialCalendar">
|
<SchedulingCalendarPage />
|
||||||
<SchedulingCalendarPage />
|
|
||||||
</FeatureGate>
|
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -309,7 +309,6 @@ function buildMenuItems(settings: import('@/types/api').SiteSettings | null, use
|
|||||||
{ key: '/app/payments/donation-pages', icon: <HeartOutlined />, label: 'Donation Pages' },
|
{ key: '/app/payments/donation-pages', icon: <HeartOutlined />, label: 'Donation Pages' },
|
||||||
{ key: '/app/payments/donations', icon: <DollarOutlined />, label: 'Donation Orders' },
|
{ key: '/app/payments/donations', icon: <DollarOutlined />, label: 'Donation Orders' },
|
||||||
{ key: '/app/payments/ads', icon: <PictureOutlined />, label: 'Gallery Ads' },
|
{ key: '/app/payments/ads', icon: <PictureOutlined />, label: 'Gallery Ads' },
|
||||||
{ key: '/app/payments/ads/analytics', icon: <BarChartOutlined />, label: 'Ad Analytics' },
|
|
||||||
{ key: '/app/payments/settings', icon: <SettingOutlined />, label: 'Settings' },
|
{ key: '/app/payments/settings', icon: <SettingOutlined />, label: 'Settings' },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@ -51,7 +51,6 @@ import type { ProductInsertResult } from '@/components/payments/ProductInsertMod
|
|||||||
import { AdPickerModal } from '@/components/media/AdPickerModal';
|
import { AdPickerModal } from '@/components/media/AdPickerModal';
|
||||||
import type { AdInsertResult } from '@/components/media/AdPickerModal';
|
import type { AdInsertResult } from '@/components/media/AdPickerModal';
|
||||||
import { PollInsertModal } from '@/components/scheduling/PollInsertModal';
|
import { PollInsertModal } from '@/components/scheduling/PollInsertModal';
|
||||||
import { WikiLinkPickerModal } from '@/components/docs/WikiLinkPickerModal';
|
|
||||||
import { useDocsCollaboration } from '@/hooks/useDocsCollaboration';
|
import { useDocsCollaboration } from '@/hooks/useDocsCollaboration';
|
||||||
import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars';
|
import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars';
|
||||||
import { YTextareaBinding } from '@/lib/y-textarea';
|
import { YTextareaBinding } from '@/lib/y-textarea';
|
||||||
@ -258,7 +257,6 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
|||||||
const [productInsertOpen, setProductInsertOpen] = useState(false);
|
const [productInsertOpen, setProductInsertOpen] = useState(false);
|
||||||
const [adPickerOpen, setAdPickerOpen] = useState(false);
|
const [adPickerOpen, setAdPickerOpen] = useState(false);
|
||||||
const [pollInsertOpen, setPollInsertOpen] = useState(false);
|
const [pollInsertOpen, setPollInsertOpen] = useState(false);
|
||||||
const [wikiLinkPickerOpen, setWikiLinkPickerOpen] = useState(false);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
fileTree,
|
fileTree,
|
||||||
@ -452,7 +450,6 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
|||||||
case 'product-card': setProductInsertOpen(true); break;
|
case 'product-card': setProductInsertOpen(true); break;
|
||||||
case 'ad-insert': setAdPickerOpen(true); break;
|
case 'ad-insert': setAdPickerOpen(true); break;
|
||||||
case 'scheduling-poll': setPollInsertOpen(true); break;
|
case 'scheduling-poll': setPollInsertOpen(true); break;
|
||||||
case 'wiki-link': setWikiLinkPickerOpen(true); break;
|
|
||||||
case 'pricing-table': {
|
case 'pricing-table': {
|
||||||
const appUrl = config
|
const appUrl = config
|
||||||
? `${window.location.protocol}//${config.domain.replace(/^([^.]+)/, 'app')}`
|
? `${window.location.protocol}//${config.domain.replace(/^([^.]+)/, 'app')}`
|
||||||
@ -888,28 +885,6 @@ export function MobileDocsEditor({ editor, collabEnabled = false }: MobileDocsEd
|
|||||||
<ProductInsertModal open={productInsertOpen} onClose={() => setProductInsertOpen(false)} onInsert={handleProductInsert} />
|
<ProductInsertModal open={productInsertOpen} onClose={() => setProductInsertOpen(false)} onInsert={handleProductInsert} />
|
||||||
<AdPickerModal open={adPickerOpen} onCancel={() => setAdPickerOpen(false)} onInsert={handleAdInsert} />
|
<AdPickerModal open={adPickerOpen} onCancel={() => setAdPickerOpen(false)} onInsert={handleAdInsert} />
|
||||||
<PollInsertModal open={pollInsertOpen} onCancel={() => setPollInsertOpen(false)} onInsert={handlePollInsert} />
|
<PollInsertModal open={pollInsertOpen} onCancel={() => setPollInsertOpen(false)} onInsert={handlePollInsert} />
|
||||||
<WikiLinkPickerModal
|
|
||||||
open={wikiLinkPickerOpen}
|
|
||||||
fileTree={fileTree}
|
|
||||||
onClose={() => setWikiLinkPickerOpen(false)}
|
|
||||||
onSelect={(wikiLink) => {
|
|
||||||
const ta = textareaRef.current;
|
|
||||||
if (ta) {
|
|
||||||
const { selectionStart, value } = ta;
|
|
||||||
const before = value.substring(0, selectionStart);
|
|
||||||
const after = value.substring(selectionStart);
|
|
||||||
onContentChange(before + wikiLink + after);
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
ta.focus();
|
|
||||||
const pos = selectionStart + wikiLink.length;
|
|
||||||
ta.setSelectionRange(pos, pos);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
onContentChange(fileContent + wikiLink);
|
|
||||||
}
|
|
||||||
setWikiLinkPickerOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import {
|
|||||||
type TextareaInsertResult,
|
type TextareaInsertResult,
|
||||||
} from '@/utils/textareaSnippets';
|
} from '@/utils/textareaSnippets';
|
||||||
|
|
||||||
export type InsertRequestType = 'video-card' | 'photo-insert' | 'donate-button' | 'pricing-table' | 'product-card' | 'ad-insert' | 'scheduling-poll' | 'wiki-link';
|
export type InsertRequestType = 'video-card' | 'photo-insert' | 'donate-button' | 'pricing-table' | 'product-card' | 'ad-insert' | 'scheduling-poll';
|
||||||
|
|
||||||
interface MobileFormattingToolbarProps {
|
interface MobileFormattingToolbarProps {
|
||||||
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
textareaRef: React.RefObject<HTMLTextAreaElement | null>;
|
||||||
@ -70,8 +70,6 @@ const MORE_SNIPPETS: SnippetDef[] = [
|
|||||||
{ id: 'footnote', label: 'Footnote', group: 'Insert', run: (ta) => insertBlock(ta, '[^1]\n\n[^1]: Text') },
|
{ id: 'footnote', label: 'Footnote', group: 'Insert', run: (ta) => insertBlock(ta, '[^1]\n\n[^1]: Text') },
|
||||||
{ id: 'def-list', label: 'Definition List', group: 'Insert', run: (ta) => insertBlock(ta, 'Term\n: Definition') },
|
{ id: 'def-list', label: 'Definition List', group: 'Insert', run: (ta) => insertBlock(ta, 'Term\n: Definition') },
|
||||||
{ id: 'hr', label: 'Horizontal Rule', group: 'Insert', run: (ta) => insertBlock(ta, '---') },
|
{ id: 'hr', label: 'Horizontal Rule', group: 'Insert', run: (ta) => insertBlock(ta, '---') },
|
||||||
// Links
|
|
||||||
{ id: 'wiki-link', label: 'Wiki Link [[]]', group: 'Links', insertType: 'wiki-link' },
|
|
||||||
// Insert — modal-based (open picker)
|
// Insert — modal-based (open picker)
|
||||||
{ id: 'video-card', label: 'Video Card', group: 'Media & Widgets', insertType: 'video-card' },
|
{ id: 'video-card', label: 'Video Card', group: 'Media & Widgets', insertType: 'video-card' },
|
||||||
{ id: 'photo-insert', label: 'Photo', group: 'Media & Widgets', insertType: 'photo-insert' },
|
{ id: 'photo-insert', label: 'Photo', group: 'Media & Widgets', insertType: 'photo-insert' },
|
||||||
|
|||||||
@ -1,153 +0,0 @@
|
|||||||
import { useState, useMemo } from 'react';
|
|
||||||
import { Modal, Input, List, theme, Typography, Tag } from 'antd';
|
|
||||||
import { FileOutlined, PictureOutlined } from '@ant-design/icons';
|
|
||||||
import type { FileNode } from '@/types/api';
|
|
||||||
|
|
||||||
interface FlatFile {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico']);
|
|
||||||
|
|
||||||
function isImage(name: string): boolean {
|
|
||||||
const dot = name.lastIndexOf('.');
|
|
||||||
return dot >= 0 && IMAGE_EXTENSIONS.has(name.substring(dot).toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenFiles(nodes: FileNode[]): FlatFile[] {
|
|
||||||
const out: FlatFile[] = [];
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (n.isDirectory) {
|
|
||||||
if (n.children) out.push(...flattenFiles(n.children));
|
|
||||||
} else {
|
|
||||||
out.push({ path: n.path, name: n.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface WikiLinkPickerModalProps {
|
|
||||||
open: boolean;
|
|
||||||
fileTree: FileNode[];
|
|
||||||
onSelect: (wikiLink: string) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function WikiLinkPickerModal({ open, fileTree, onSelect, onClose }: WikiLinkPickerModalProps) {
|
|
||||||
const { token } = theme.useToken();
|
|
||||||
const [search, setSearch] = useState('');
|
|
||||||
|
|
||||||
const allFiles = useMemo(() => flattenFiles(fileTree), [fileTree]);
|
|
||||||
|
|
||||||
const filtered = useMemo(() => {
|
|
||||||
if (!search.trim()) return allFiles;
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
return allFiles.filter(
|
|
||||||
f => f.name.toLowerCase().includes(q) || f.path.toLowerCase().includes(q)
|
|
||||||
);
|
|
||||||
}, [allFiles, search]);
|
|
||||||
|
|
||||||
// Group: images first if searching for images, docs first otherwise
|
|
||||||
const docs = useMemo(() => filtered.filter(f => !isImage(f.name)), [filtered]);
|
|
||||||
const images = useMemo(() => filtered.filter(f => isImage(f.name)), [filtered]);
|
|
||||||
|
|
||||||
const handleSelect = (file: FlatFile) => {
|
|
||||||
const img = isImage(file.name);
|
|
||||||
const linkName = file.name.endsWith('.md') ? file.name.slice(0, -3) : file.name;
|
|
||||||
// For images, use ![[name]] syntax; for docs, use [[name]]
|
|
||||||
const wikiLink = img ? `![[${linkName}]]` : `[[${linkName}]]`;
|
|
||||||
onSelect(wikiLink);
|
|
||||||
setSearch('');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title="Insert Wiki Link"
|
|
||||||
open={open}
|
|
||||||
onCancel={() => { onClose(); setSearch(''); }}
|
|
||||||
footer={null}
|
|
||||||
destroyOnHidden
|
|
||||||
width={420}
|
|
||||||
>
|
|
||||||
<Input.Search
|
|
||||||
placeholder="Search files..."
|
|
||||||
value={search}
|
|
||||||
onChange={e => setSearch(e.target.value)}
|
|
||||||
allowClear
|
|
||||||
autoFocus
|
|
||||||
style={{ marginBottom: 12 }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div style={{ maxHeight: 360, overflow: 'auto' }}>
|
|
||||||
{docs.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, padding: '4px 0', display: 'block' }}>
|
|
||||||
Documents
|
|
||||||
</Typography.Text>
|
|
||||||
<List
|
|
||||||
size="small"
|
|
||||||
dataSource={docs.slice(0, 30)}
|
|
||||||
renderItem={item => (
|
|
||||||
<List.Item
|
|
||||||
style={{ padding: '8px 8px', cursor: 'pointer' }}
|
|
||||||
onClick={() => handleSelect(item)}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, width: '100%' }}>
|
|
||||||
<FileOutlined style={{ color: token.colorTextSecondary, flexShrink: 0 }} />
|
|
||||||
<div style={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{item.name.replace(/\.md$/, '')}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 11, color: token.colorTextTertiary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{item.path}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Tag color="blue" style={{ marginInlineEnd: 0 }}>doc</Tag>
|
|
||||||
</div>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{images.length > 0 && (
|
|
||||||
<>
|
|
||||||
<Typography.Text type="secondary" style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.5, padding: '4px 0', display: 'block' }}>
|
|
||||||
Images
|
|
||||||
</Typography.Text>
|
|
||||||
<List
|
|
||||||
size="small"
|
|
||||||
dataSource={images.slice(0, 20)}
|
|
||||||
renderItem={item => (
|
|
||||||
<List.Item
|
|
||||||
style={{ padding: '8px 8px', cursor: 'pointer' }}
|
|
||||||
onClick={() => handleSelect(item)}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, width: '100%' }}>
|
|
||||||
<PictureOutlined style={{ color: token.colorTextSecondary, flexShrink: 0 }} />
|
|
||||||
<div style={{ minWidth: 0, flex: 1 }}>
|
|
||||||
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: 11, color: token.colorTextTertiary, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
|
||||||
{item.path}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Tag color="green" style={{ marginInlineEnd: 0 }}>img</Tag>
|
|
||||||
</div>
|
|
||||||
</List.Item>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{docs.length === 0 && images.length === 0 && (
|
|
||||||
<div style={{ textAlign: 'center', padding: 24, color: token.colorTextTertiary }}>
|
|
||||||
No files found
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -124,7 +124,7 @@ export function buildFeatureFlags(settings: Record<string, any> | null | undefin
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Check whether a single feature flag passes */
|
/** Check whether a single feature flag passes */
|
||||||
export function flagPasses(flagName: string, flags: Record<string, boolean | undefined>): boolean {
|
function flagPasses(flagName: string, flags: Record<string, boolean | undefined>): boolean {
|
||||||
if (OPT_OUT_FLAGS.has(flagName)) {
|
if (OPT_OUT_FLAGS.has(flagName)) {
|
||||||
return flags[flagName] !== false;
|
return flags[flagName] !== false;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,14 @@ import dayjs from 'dayjs';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { AdminCalendarView } from '@/types/api';
|
import type { AdminCalendarView } from '@/types/api';
|
||||||
import type { AppOutletContext } from '@/components/AppLayout';
|
import type { AppOutletContext } from '@/components/AppLayout';
|
||||||
import { ROLE_COLORS, ROLE_OPTIONS } from '@/utils/role-constants';
|
|
||||||
|
const ROLE_OPTIONS = [
|
||||||
|
{ label: 'Super Admin', value: 'SUPER_ADMIN' },
|
||||||
|
{ label: 'Influence Admin', value: 'INFLUENCE_ADMIN' },
|
||||||
|
{ label: 'Map Admin', value: 'MAP_ADMIN' },
|
||||||
|
{ label: 'User', value: 'USER' },
|
||||||
|
{ label: 'Temp', value: 'TEMP' },
|
||||||
|
];
|
||||||
|
|
||||||
const LAYER_TYPE_OPTIONS = [
|
const LAYER_TYPE_OPTIONS = [
|
||||||
{ label: 'Shifts', value: 'SHIFTS' },
|
{ label: 'Shifts', value: 'SHIFTS' },
|
||||||
@ -26,6 +33,14 @@ const LAYER_TYPE_OPTIONS = [
|
|||||||
{ label: 'Public Events', value: 'PUBLIC_EVENTS' },
|
{ label: 'Public Events', value: 'PUBLIC_EVENTS' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ROLE_COLORS: Record<string, string> = {
|
||||||
|
SUPER_ADMIN: 'red',
|
||||||
|
INFLUENCE_ADMIN: 'blue',
|
||||||
|
MAP_ADMIN: 'green',
|
||||||
|
USER: 'default',
|
||||||
|
TEMP: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdminCalendarPage() {
|
export default function AdminCalendarPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { setPageHeader } = useOutletContext<AppOutletContext>();
|
const { setPageHeader } = useOutletContext<AppOutletContext>();
|
||||||
|
|||||||
@ -29,10 +29,17 @@ import type {
|
|||||||
AdminCalendarUser,
|
AdminCalendarUser,
|
||||||
AdminCalendarItem,
|
AdminCalendarItem,
|
||||||
} from '@/types/api';
|
} from '@/types/api';
|
||||||
import { ROLE_COLORS } from '@/utils/role-constants';
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
|
const ROLE_COLORS: Record<string, string> = {
|
||||||
|
SUPER_ADMIN: 'red',
|
||||||
|
INFLUENCE_ADMIN: 'blue',
|
||||||
|
MAP_ADMIN: 'green',
|
||||||
|
USER: 'default',
|
||||||
|
TEMP: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
export default function AdminCalendarViewPage() {
|
export default function AdminCalendarViewPage() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@ -89,8 +89,6 @@ import { useDocsCollaboration } from '@/hooks/useDocsCollaboration';
|
|||||||
import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars';
|
import { CollaboratorAvatars } from '@/components/docs/CollaboratorAvatars';
|
||||||
import { MonacoBinding } from 'y-monaco';
|
import { MonacoBinding } from 'y-monaco';
|
||||||
import type { SiteSettings } from '@/types/api';
|
import type { SiteSettings } from '@/types/api';
|
||||||
import { registerWikiLinkCompletion } from '@/utils/wikiLinkCompletion';
|
|
||||||
import { WikiLinkPickerModal } from '@/components/docs/WikiLinkPickerModal';
|
|
||||||
|
|
||||||
type LayoutMode = 'split' | 'editor' | 'preview';
|
type LayoutMode = 'split' | 'editor' | 'preview';
|
||||||
type PreviewMode = 'desktop' | 'mobile';
|
type PreviewMode = 'desktop' | 'mobile';
|
||||||
@ -370,7 +368,6 @@ const SNIPPETS: MkDocsSnippet[] = [
|
|||||||
{ id: 'product-card', label: 'Product Card', group: 'insert', type: 'insert', template: '' },
|
{ id: 'product-card', label: 'Product Card', group: 'insert', type: 'insert', template: '' },
|
||||||
{ id: 'ad-insert', label: 'Ad', group: 'insert', type: 'insert', template: '' },
|
{ id: 'ad-insert', label: 'Ad', group: 'insert', type: 'insert', template: '' },
|
||||||
{ id: 'scheduling-poll', label: 'Scheduling Poll', group: 'insert', type: 'insert', template: '' },
|
{ id: 'scheduling-poll', label: 'Scheduling Poll', group: 'insert', type: 'insert', template: '' },
|
||||||
{ id: 'wiki-link', label: 'Wiki Link [[]]', group: 'insert', type: 'insert', template: '' },
|
|
||||||
{ id: 'hr', label: 'Horizontal Rule', group: 'insert', type: 'insert', template: '---' },
|
{ id: 'hr', label: 'Horizontal Rule', group: 'insert', type: 'insert', template: '---' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -615,7 +612,6 @@ export default function DocsPage() {
|
|||||||
const [productInsertOpen, setProductInsertOpen] = useState(false);
|
const [productInsertOpen, setProductInsertOpen] = useState(false);
|
||||||
const [adPickerOpen, setAdPickerOpen] = useState(false);
|
const [adPickerOpen, setAdPickerOpen] = useState(false);
|
||||||
const [pollInsertOpen, setPollInsertOpen] = useState(false);
|
const [pollInsertOpen, setPollInsertOpen] = useState(false);
|
||||||
const [wikiLinkPickerOpen, setWikiLinkPickerOpen] = useState(false);
|
|
||||||
const [dragOver, setDragOver] = useState(false);
|
const [dragOver, setDragOver] = useState(false);
|
||||||
const dragCounter = useRef(0);
|
const dragCounter = useRef(0);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@ -627,7 +623,6 @@ export default function DocsPage() {
|
|||||||
const monacoEditorRef = useRef<monacoEditor.IStandaloneCodeEditor | null>(null);
|
const monacoEditorRef = useRef<monacoEditor.IStandaloneCodeEditor | null>(null);
|
||||||
const monacoRef = useRef<typeof import('monaco-editor') | null>(null);
|
const monacoRef = useRef<typeof import('monaco-editor') | null>(null);
|
||||||
const monacoBindingRef = useRef<MonacoBinding | null>(null);
|
const monacoBindingRef = useRef<MonacoBinding | null>(null);
|
||||||
const fileTreeRef = useRef<FileNode[]>([]);
|
|
||||||
const [editorReady, setEditorReady] = useState(false);
|
const [editorReady, setEditorReady] = useState(false);
|
||||||
|
|
||||||
// --- Collaboration state ---
|
// --- Collaboration state ---
|
||||||
@ -645,9 +640,6 @@ export default function DocsPage() {
|
|||||||
|
|
||||||
const [messageApi, contextHolder] = message.useMessage();
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
|
||||||
// Keep fileTreeRef in sync for Monaco autocomplete callback
|
|
||||||
useEffect(() => { fileTreeRef.current = fileTree; }, [fileTree]);
|
|
||||||
|
|
||||||
// Fetch file tree
|
// Fetch file tree
|
||||||
const fetchTree = useCallback(async (showLoading = true, force = false) => {
|
const fetchTree = useCallback(async (showLoading = true, force = false) => {
|
||||||
try {
|
try {
|
||||||
@ -827,9 +819,6 @@ export default function DocsPage() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Register wiki-link [[ autocomplete
|
|
||||||
registerWikiLinkCompletion(monaco, () => fileTreeRef.current);
|
|
||||||
|
|
||||||
// Custom right-click context menu (replaces Monaco's flat list)
|
// Custom right-click context menu (replaces Monaco's flat list)
|
||||||
const domNode = ed.getDomNode();
|
const domNode = ed.getDomNode();
|
||||||
if (domNode) {
|
if (domNode) {
|
||||||
@ -923,12 +912,6 @@ export default function DocsPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wiki link — opens file picker or inserts [[ for autocomplete
|
|
||||||
if (snippetId === 'wiki-link') {
|
|
||||||
setWikiLinkPickerOpen(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pricing table — static CTA (plans are dynamic, so link out)
|
// Pricing table — static CTA (plans are dynamic, so link out)
|
||||||
if (snippetId === 'pricing-table') {
|
if (snippetId === 'pricing-table') {
|
||||||
const appUrl = config
|
const appUrl = config
|
||||||
@ -2108,7 +2091,7 @@ export default function DocsPage() {
|
|||||||
<Dropdown menu={{ items: SNIPPETS.filter(s => s.group === 'insert').map(s => ({
|
<Dropdown menu={{ items: SNIPPETS.filter(s => s.group === 'insert').map(s => ({
|
||||||
key: s.id,
|
key: s.id,
|
||||||
label: s.label,
|
label: s.label,
|
||||||
icon: s.id === 'wiki-link' ? <NodeExpandOutlined /> : s.id === 'video-card' ? <PlayCircleOutlined /> : s.id === 'photo-insert' ? <PictureOutlined /> : s.id === 'donate-button' ? <HeartOutlined /> : s.id === 'pricing-table' ? <CrownOutlined /> : s.id === 'product-card' ? <ShoppingCartOutlined /> : s.id === 'ad-insert' ? <BuildOutlined /> : s.id === 'scheduling-poll' ? <CalendarOutlined /> : s.id === 'link' ? <LinkOutlined /> : s.id === 'image' ? <FileMarkdownOutlined /> : s.id === 'table' ? <TableOutlined /> : <PlusOutlined />,
|
icon: s.id === 'video-card' ? <PlayCircleOutlined /> : s.id === 'photo-insert' ? <PictureOutlined /> : s.id === 'donate-button' ? <HeartOutlined /> : s.id === 'pricing-table' ? <CrownOutlined /> : s.id === 'product-card' ? <ShoppingCartOutlined /> : s.id === 'ad-insert' ? <BuildOutlined /> : s.id === 'scheduling-poll' ? <CalendarOutlined /> : s.id === 'link' ? <LinkOutlined /> : s.id === 'image' ? <FileMarkdownOutlined /> : s.id === 'table' ? <TableOutlined /> : <PlusOutlined />,
|
||||||
onClick: () => handleToolbarSnippet(s.id),
|
onClick: () => handleToolbarSnippet(s.id),
|
||||||
})) }} trigger={['click']}>
|
})) }} trigger={['click']}>
|
||||||
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
<Button type="text" size="small" style={{ height: 24, fontSize: 12 }}>
|
||||||
@ -2328,23 +2311,6 @@ export default function DocsPage() {
|
|||||||
onInsert={handlePollInsert}
|
onInsert={handlePollInsert}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<WikiLinkPickerModal
|
|
||||||
open={wikiLinkPickerOpen}
|
|
||||||
fileTree={fileTree}
|
|
||||||
onClose={() => setWikiLinkPickerOpen(false)}
|
|
||||||
onSelect={(wikiLink) => {
|
|
||||||
const ed = monacoEditorRef.current;
|
|
||||||
if (ed) {
|
|
||||||
const sel = ed.getSelection();
|
|
||||||
if (sel) {
|
|
||||||
ed.executeEdits('wiki-link-insert', [{ range: sel, text: wikiLink }]);
|
|
||||||
ed.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setWikiLinkPickerOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Custom right-click context menu with submenus */}
|
{/* Custom right-click context menu with submenus */}
|
||||||
{ctxMenu && (
|
{ctxMenu && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -808,27 +808,6 @@ export default function MkDocsSettingsPage() {
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
children: (
|
children: (
|
||||||
<div style={{ maxWidth: 900 }}>
|
<div style={{ maxWidth: 900 }}>
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16, gap: 8 }}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
icon={<SaveOutlined />}
|
|
||||||
onClick={saveSettings}
|
|
||||||
loading={settingsSaving}
|
|
||||||
disabled={!settingsDirty || !isSuperAdmin}
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</Button>
|
|
||||||
<Tooltip title="Build static site">
|
|
||||||
<Button
|
|
||||||
icon={building ? <LoadingOutlined /> : <BuildOutlined />}
|
|
||||||
onClick={triggerBuild}
|
|
||||||
loading={building}
|
|
||||||
disabled={!isSuperAdmin}
|
|
||||||
>
|
|
||||||
Build
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<Card title="Site Info" size="small" style={{ marginBottom: 16 }}>
|
<Card title="Site Info" size="small" style={{ marginBottom: 16 }}>
|
||||||
<Form
|
<Form
|
||||||
form={settingsForm}
|
form={settingsForm}
|
||||||
@ -912,6 +891,17 @@ export default function MkDocsSettingsPage() {
|
|||||||
))}
|
))}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '16px 0' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
onClick={saveSettings}
|
||||||
|
loading={settingsSaving}
|
||||||
|
disabled={!settingsDirty || !isSuperAdmin}
|
||||||
|
>
|
||||||
|
Save Settings
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -1161,7 +1151,7 @@ export default function MkDocsSettingsPage() {
|
|||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '16px 0', gap: 8 }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end', padding: '16px 0' }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
@ -1171,16 +1161,6 @@ export default function MkDocsSettingsPage() {
|
|||||||
>
|
>
|
||||||
Save Navigation
|
Save Navigation
|
||||||
</Button>
|
</Button>
|
||||||
<Tooltip title="Build static site">
|
|
||||||
<Button
|
|
||||||
icon={building ? <LoadingOutlined /> : <BuildOutlined />}
|
|
||||||
onClick={triggerBuild}
|
|
||||||
loading={building}
|
|
||||||
disabled={!isSuperAdmin}
|
|
||||||
>
|
|
||||||
Build
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nav Add/Edit Modal */}
|
{/* Nav Add/Edit Modal */}
|
||||||
@ -1243,27 +1223,15 @@ export default function MkDocsSettingsPage() {
|
|||||||
<Space>
|
<Space>
|
||||||
{editorDirty && <Text type="warning">Modified</Text>}
|
{editorDirty && <Text type="warning">Modified</Text>}
|
||||||
</Space>
|
</Space>
|
||||||
<Space>
|
<Button
|
||||||
<Button
|
type="primary"
|
||||||
type="primary"
|
icon={<SaveOutlined />}
|
||||||
icon={<SaveOutlined />}
|
onClick={saveEditor}
|
||||||
onClick={saveEditor}
|
loading={editorSaving}
|
||||||
loading={editorSaving}
|
disabled={!editorDirty || !isSuperAdmin}
|
||||||
disabled={!editorDirty || !isSuperAdmin}
|
>
|
||||||
>
|
Save (Ctrl+S)
|
||||||
Save (Ctrl+S)
|
</Button>
|
||||||
</Button>
|
|
||||||
<Tooltip title="Build static site">
|
|
||||||
<Button
|
|
||||||
icon={building ? <LoadingOutlined /> : <BuildOutlined />}
|
|
||||||
onClick={triggerBuild}
|
|
||||||
loading={building}
|
|
||||||
disabled={!isSuperAdmin}
|
|
||||||
>
|
|
||||||
Build
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Space>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, minHeight: 0, border: `1px solid ${token.colorBorderSecondary}`, borderRadius: 4 }}>
|
<div style={{ flex: 1, minHeight: 0, border: `1px solid ${token.colorBorderSecondary}`, borderRadius: 4 }}>
|
||||||
<Editor
|
<Editor
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useNavigate, useOutletContext } from 'react-router-dom';
|
import { useOutletContext } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
Switch,
|
Switch,
|
||||||
@ -24,7 +24,6 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
FolderAddOutlined,
|
FolderAddOutlined,
|
||||||
ExclamationCircleOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import type { AppOutletContext } from '@/components/AppLayout';
|
import type { AppOutletContext } from '@/components/AppLayout';
|
||||||
@ -33,8 +32,6 @@ import {
|
|||||||
DEFAULT_NAV_ITEMS,
|
DEFAULT_NAV_ITEMS,
|
||||||
ICON_MAP,
|
ICON_MAP,
|
||||||
mergeNavDefaults,
|
mergeNavDefaults,
|
||||||
buildFeatureFlags,
|
|
||||||
flagPasses,
|
|
||||||
} from '@/lib/nav-defaults';
|
} from '@/lib/nav-defaults';
|
||||||
|
|
||||||
export default function NavigationSettingsPage() {
|
export default function NavigationSettingsPage() {
|
||||||
@ -48,22 +45,6 @@ export default function NavigationSettingsPage() {
|
|||||||
const [customLinkLabel, setCustomLinkLabel] = useState('');
|
const [customLinkLabel, setCustomLinkLabel] = useState('');
|
||||||
const [customLinkPath, setCustomLinkPath] = useState('');
|
const [customLinkPath, setCustomLinkPath] = useState('');
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const featureFlags = useMemo(() => buildFeatureFlags(settings), [settings]);
|
|
||||||
|
|
||||||
const isFeatureFlagDisabled = (item: NavItem): boolean => {
|
|
||||||
if (!item.featureFlag) return false;
|
|
||||||
return !flagPasses(item.featureFlag, featureFlags);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if any item has a disabled feature flag (for warning banner)
|
|
||||||
const hasDisabledFlags = useMemo(() => {
|
|
||||||
return navItems.some(item => {
|
|
||||||
if (isFeatureFlagDisabled(item)) return true;
|
|
||||||
return item.children?.some(c => isFeatureFlagDisabled(c)) ?? false;
|
|
||||||
});
|
|
||||||
}, [navItems, featureFlags]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPageHeader({ title: 'Navigation' });
|
setPageHeader({ title: 'Navigation' });
|
||||||
return () => setPageHeader(null);
|
return () => setPageHeader(null);
|
||||||
@ -273,13 +254,11 @@ export default function NavigationSettingsPage() {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderItemRow = (item: NavItem, _idx: number, siblings: NavItem[], indent: boolean, parentFlagDisabled = false) => {
|
const renderItemRow = (item: NavItem, _idx: number, siblings: NavItem[], indent: boolean) => {
|
||||||
const isGroup = item.type === 'group';
|
const isGroup = item.type === 'group';
|
||||||
const sorted = [...siblings].sort((a, b) => a.order - b.order);
|
const sorted = [...siblings].sort((a, b) => a.order - b.order);
|
||||||
const sortedIdx = sorted.findIndex(i => i.id === item.id);
|
const sortedIdx = sorted.findIndex(i => i.id === item.id);
|
||||||
const parentGroupId = indent ? findParentGroupId(item.id) : null;
|
const parentGroupId = indent ? findParentGroupId(item.id) : null;
|
||||||
const ownFlagDisabled = isFeatureFlagDisabled(item);
|
|
||||||
const flagDisabled = ownFlagDisabled || parentFlagDisabled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -300,10 +279,8 @@ export default function NavigationSettingsPage() {
|
|||||||
border: isGroup
|
border: isGroup
|
||||||
? '1px solid rgba(100,150,255,0.15)'
|
? '1px solid rgba(100,150,255,0.15)'
|
||||||
: '1px solid rgba(255,255,255,0.08)',
|
: '1px solid rgba(255,255,255,0.08)',
|
||||||
borderLeft: flagDisabled
|
borderLeft: indent ? '3px solid rgba(100,150,255,0.3)' : undefined,
|
||||||
? '3px dashed rgba(250,173,20,0.6)'
|
opacity: item.enabled ? 1 : 0.5,
|
||||||
: indent ? '3px solid rgba(100,150,255,0.3)' : undefined,
|
|
||||||
opacity: item.enabled ? (flagDisabled ? 0.55 : 1) : 0.5,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Switch
|
<Switch
|
||||||
@ -385,46 +362,21 @@ export default function NavigationSettingsPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div style={{ textAlign: 'right' }}>
|
<div style={{ textAlign: 'right' }}>
|
||||||
{item.featureFlag ? (
|
{item.featureFlag ? (
|
||||||
ownFlagDisabled ? (
|
|
||||||
<Tooltip title={<span>Feature disabled in Settings. <a onClick={() => navigate('/app/settings', { state: { tab: 'features' } })} style={{ color: '#69b1ff', cursor: 'pointer' }}>Feature Toggles</a></span>}>
|
|
||||||
<Tag color="orange" style={{ margin: 0, fontSize: 10, cursor: 'help' }}>
|
|
||||||
<ExclamationCircleOutlined style={{ marginRight: 3 }} />
|
|
||||||
{item.featureFlag.replace('enable', '')} Off
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={`Controlled by ${item.featureFlag}`}>
|
|
||||||
<Tag color="cyan" style={{ margin: 0, fontSize: 10 }}>
|
|
||||||
{item.featureFlag.replace('enable', '')}
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Tag color="geekblue" style={{ margin: 0, fontSize: 10 }}>group</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{/* Feature flag tag for non-group items */}
|
|
||||||
{!isGroup && item.featureFlag ? (
|
|
||||||
<div style={{ textAlign: 'right' }}>
|
|
||||||
{ownFlagDisabled ? (
|
|
||||||
<Tooltip title={<span>Feature disabled in Settings. <a onClick={() => navigate('/app/settings', { state: { tab: 'features' } })} style={{ color: '#69b1ff', cursor: 'pointer' }}>Feature Toggles</a></span>}>
|
|
||||||
<Tag color="orange" style={{ margin: 0, fontSize: 10, cursor: 'help' }}>
|
|
||||||
<ExclamationCircleOutlined style={{ marginRight: 3 }} />
|
|
||||||
{item.featureFlag.replace('enable', '')} Off
|
|
||||||
</Tag>
|
|
||||||
</Tooltip>
|
|
||||||
) : (
|
|
||||||
<Tooltip title={`Controlled by ${item.featureFlag}`}>
|
<Tooltip title={`Controlled by ${item.featureFlag}`}>
|
||||||
<Tag color="cyan" style={{ margin: 0, fontSize: 10 }}>
|
<Tag color="cyan" style={{ margin: 0, fontSize: 10 }}>
|
||||||
{item.featureFlag.replace('enable', '')}
|
{item.featureFlag.replace('enable', '')}
|
||||||
</Tag>
|
</Tag>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
<Tag color="geekblue" style={{ margin: 0, fontSize: 10 }}>group</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : !isGroup ? (
|
)}
|
||||||
<div />
|
{!isGroup && (
|
||||||
) : null}
|
<div style={{ display: 'none' }}>
|
||||||
|
{/* Placeholder — tag column handled by the Select above */}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -437,25 +389,8 @@ export default function NavigationSettingsPage() {
|
|||||||
type="info"
|
type="info"
|
||||||
message="Configure the navigation bar shown on all public pages, the admin header, Gancio events page, and MkDocs site. Groups appear as dropdowns on desktop and collapsible sections on mobile."
|
message="Configure the navigation bar shown on all public pages, the admin header, Gancio events page, and MkDocs site. Groups appear as dropdowns on desktop and collapsible sections on mobile."
|
||||||
showIcon
|
showIcon
|
||||||
style={{ marginBottom: hasDisabledFlags ? 12 : 24 }}
|
style={{ marginBottom: 24 }}
|
||||||
/>
|
/>
|
||||||
{hasDisabledFlags && (
|
|
||||||
<Alert
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
message={
|
|
||||||
<span>
|
|
||||||
Some nav items have their feature disabled in Settings.
|
|
||||||
Items with <Tag color="orange" style={{ margin: '0 4px', fontSize: 10 }}><ExclamationCircleOutlined style={{ marginRight: 3 }} />Off</Tag>
|
|
||||||
won't appear in public navigation until the feature is enabled.{' '}
|
|
||||||
<Button type="link" size="small" style={{ padding: 0 }} onClick={() => navigate('/app/settings', { state: { tab: 'features' } })}>
|
|
||||||
Feature Toggles
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
style={{ marginBottom: 24 }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 16 }}>
|
||||||
{sorted.map((item, idx) => (
|
{sorted.map((item, idx) => (
|
||||||
@ -463,7 +398,7 @@ export default function NavigationSettingsPage() {
|
|||||||
{renderItemRow(item, idx, sorted, false)}
|
{renderItemRow(item, idx, sorted, false)}
|
||||||
{/* Render children indented below their group */}
|
{/* Render children indented below their group */}
|
||||||
{item.type === 'group' && item.children && [...item.children].sort((a, b) => a.order - b.order).map((child, childIdx) =>
|
{item.type === 'group' && item.children && [...item.children].sort((a, b) => a.order - b.order).map((child, childIdx) =>
|
||||||
renderItemRow(child, childIdx, item.children!, true, isFeatureFlagDisabled(item))
|
renderItemRow(child, childIdx, item.children!, true)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -24,13 +24,20 @@ import UnifiedCalendar from '@/components/calendar/UnifiedCalendar';
|
|||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { UnifiedCalendarItem, AdminCalendarView } from '@/types/api';
|
import type { UnifiedCalendarItem, AdminCalendarView } from '@/types/api';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { ROLE_COLORS, ROLE_OPTIONS } from '@/utils/role-constants';
|
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
|
|
||||||
const VIEWS_PANEL_WIDTH = 480;
|
const VIEWS_PANEL_WIDTH = 480;
|
||||||
const FORM_PANEL_WIDTH = 380;
|
const FORM_PANEL_WIDTH = 380;
|
||||||
|
|
||||||
|
const ROLE_OPTIONS = [
|
||||||
|
{ label: 'Super Admin', value: 'SUPER_ADMIN' },
|
||||||
|
{ label: 'Influence Admin', value: 'INFLUENCE_ADMIN' },
|
||||||
|
{ label: 'Map Admin', value: 'MAP_ADMIN' },
|
||||||
|
{ label: 'User', value: 'USER' },
|
||||||
|
{ label: 'Temp', value: 'TEMP' },
|
||||||
|
];
|
||||||
|
|
||||||
const LAYER_TYPE_OPTIONS = [
|
const LAYER_TYPE_OPTIONS = [
|
||||||
{ label: 'Shifts', value: 'SHIFTS' },
|
{ label: 'Shifts', value: 'SHIFTS' },
|
||||||
{ label: 'Tickets', value: 'TICKETS' },
|
{ label: 'Tickets', value: 'TICKETS' },
|
||||||
@ -38,6 +45,14 @@ const LAYER_TYPE_OPTIONS = [
|
|||||||
{ label: 'Public Events', value: 'PUBLIC_EVENTS' },
|
{ label: 'Public Events', value: 'PUBLIC_EVENTS' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const ROLE_COLORS: Record<string, string> = {
|
||||||
|
SUPER_ADMIN: 'red',
|
||||||
|
INFLUENCE_ADMIN: 'blue',
|
||||||
|
MAP_ADMIN: 'green',
|
||||||
|
USER: 'default',
|
||||||
|
TEMP: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
export default function SchedulingCalendarPage() {
|
export default function SchedulingCalendarPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const addEventRef = useRef<(() => void) | null>(null);
|
const addEventRef = useRef<(() => void) | null>(null);
|
||||||
|
|||||||
@ -121,7 +121,6 @@ export default function ShiftsPage() {
|
|||||||
const [activeTab, setActiveTab] = useState<'table' | 'calendar'>('table');
|
const [activeTab, setActiveTab] = useState<'table' | 'calendar'>('table');
|
||||||
const [editModeModalOpen, setEditModeModalOpen] = useState(false);
|
const [editModeModalOpen, setEditModeModalOpen] = useState(false);
|
||||||
const [editingSeriesShift, setEditingSeriesShift] = useState<Shift | null>(null);
|
const [editingSeriesShift, setEditingSeriesShift] = useState<Shift | null>(null);
|
||||||
const [seriesShiftCount, setSeriesShiftCount] = useState(0);
|
|
||||||
const [calendarData, setCalendarData] = useState<CalendarData['dates']>({});
|
const [calendarData, setCalendarData] = useState<CalendarData['dates']>({});
|
||||||
const [calendarLoading, setCalendarLoading] = useState(false);
|
const [calendarLoading, setCalendarLoading] = useState(false);
|
||||||
const [currentMonth] = useState(dayjs());
|
const [currentMonth] = useState(dayjs());
|
||||||
@ -356,12 +355,6 @@ export default function ShiftsPage() {
|
|||||||
// Part of a series - show edit mode modal
|
// Part of a series - show edit mode modal
|
||||||
setEditingSeriesShift(shift);
|
setEditingSeriesShift(shift);
|
||||||
setEditModeModalOpen(true);
|
setEditModeModalOpen(true);
|
||||||
// Fetch series shift count
|
|
||||||
if (shift.seriesId) {
|
|
||||||
api.get(`/api/map/shifts/series/${shift.seriesId}/count`)
|
|
||||||
.then((res) => setSeriesShiftCount(res.data.count ?? 0))
|
|
||||||
.catch(() => setSeriesShiftCount(0));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Regular shift or exception - edit normally
|
// Regular shift or exception - edit normally
|
||||||
openEdit(shift);
|
openEdit(shift);
|
||||||
@ -1214,7 +1207,7 @@ export default function ShiftsPage() {
|
|||||||
}}
|
}}
|
||||||
onConfirm={handleEditMode}
|
onConfirm={handleEditMode}
|
||||||
shiftDate={editingSeriesShift?.date || ''}
|
shiftDate={editingSeriesShift?.date || ''}
|
||||||
shiftsCount={seriesShiftCount}
|
shiftsCount={0} // TODO: fetch series shifts count
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -36,6 +36,8 @@ import {
|
|||||||
EditOutlined,
|
EditOutlined,
|
||||||
EnvironmentOutlined,
|
EnvironmentOutlined,
|
||||||
BankOutlined,
|
BankOutlined,
|
||||||
|
ScheduleOutlined,
|
||||||
|
PlayCircleOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
@ -527,6 +529,40 @@ export default function CampaignsListPage() {
|
|||||||
</Row>
|
</Row>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Explore More Section */}
|
||||||
|
{(() => {
|
||||||
|
const links = [
|
||||||
|
{ to: '/map', icon: <EnvironmentOutlined />, label: 'Interactive Map', desc: 'Explore locations on the map', show: siteSettings?.enableMap !== false },
|
||||||
|
{ to: '/shifts', icon: <ScheduleOutlined />, label: 'Volunteer Shifts', desc: 'Sign up for upcoming events', show: siteSettings?.enableMap !== false },
|
||||||
|
{ to: '/gallery', icon: <PlayCircleOutlined />, label: 'Media Gallery', desc: 'Watch videos and media', show: siteSettings?.enableMediaFeatures !== false },
|
||||||
|
].filter(l => l.show);
|
||||||
|
if (links.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<div style={{ marginTop: 48, textAlign: 'center' }}>
|
||||||
|
<Divider style={{ borderColor: 'rgba(255,255,255,0.08)' }}>
|
||||||
|
<Text style={{ color: 'rgba(255,255,255,0.45)', fontSize: 13 }}>Explore More</Text>
|
||||||
|
</Divider>
|
||||||
|
<Row gutter={[16, 16]} justify="center" style={{ marginTop: 16 }}>
|
||||||
|
{links.map(link => (
|
||||||
|
<Col xs={24} sm={8} key={link.to}>
|
||||||
|
<Link to={link.to} style={{ textDecoration: 'none' }}>
|
||||||
|
<Card
|
||||||
|
hoverable
|
||||||
|
size="small"
|
||||||
|
style={{ background: colorBgContainer, border: '1px solid rgba(255,255,255,0.08)', textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
<div style={{ fontSize: 24, marginBottom: 8, color: colorPrimary }}>{link.icon}</div>
|
||||||
|
<Text strong style={{ display: 'block' }}>{link.label}</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>{link.desc}</Text>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
</Col>
|
||||||
|
))}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
<AuthModal
|
<AuthModal
|
||||||
open={authModalOpen}
|
open={authModalOpen}
|
||||||
onCancel={() => setAuthModalOpen(false)}
|
onCancel={() => setAuthModalOpen(false)}
|
||||||
|
|||||||
@ -14,10 +14,25 @@ import { ReactFlowProvider } from '@xyflow/react';
|
|||||||
import SocialNetworkGraph, { type GraphData } from '@/components/social/SocialNetworkGraph';
|
import SocialNetworkGraph, { type GraphData } from '@/components/social/SocialNetworkGraph';
|
||||||
import { api } from '@/lib/api';
|
import { api } from '@/lib/api';
|
||||||
import type { AppOutletContext } from '@/types/api';
|
import type { AppOutletContext } from '@/types/api';
|
||||||
import { ROLE_COLORS, ROLE_FILTER_OPTIONS } from '@/utils/role-constants';
|
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
|
|
||||||
|
const ROLE_COLORS: Record<string, string> = {
|
||||||
|
SUPER_ADMIN: 'red',
|
||||||
|
INFLUENCE_ADMIN: 'blue',
|
||||||
|
MAP_ADMIN: 'green',
|
||||||
|
USER: 'default',
|
||||||
|
TEMP: 'orange',
|
||||||
|
};
|
||||||
|
|
||||||
|
const ROLE_OPTIONS = [
|
||||||
|
{ label: 'All Roles', value: '' },
|
||||||
|
{ label: 'Super Admin', value: 'SUPER_ADMIN' },
|
||||||
|
{ label: 'Influence Admin', value: 'INFLUENCE_ADMIN' },
|
||||||
|
{ label: 'Map Admin', value: 'MAP_ADMIN' },
|
||||||
|
{ label: 'User', value: 'USER' },
|
||||||
|
];
|
||||||
|
|
||||||
type LayoutMode = 'force' | 'radial';
|
type LayoutMode = 'force' | 'radial';
|
||||||
|
|
||||||
interface SelectedUser {
|
interface SelectedUser {
|
||||||
@ -130,7 +145,7 @@ function GraphPageInner() {
|
|||||||
<Select
|
<Select
|
||||||
value={roleFilter}
|
value={roleFilter}
|
||||||
onChange={setRoleFilter}
|
onChange={setRoleFilter}
|
||||||
options={ROLE_FILTER_OPTIONS}
|
options={ROLE_OPTIONS}
|
||||||
style={{ width: 150 }}
|
style={{ width: 150 }}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -18,11 +18,6 @@ const TYPE_LABELS: Record<string, { label: string; color: string }> = {
|
|||||||
upload_rejected: { label: 'Rejected', color: 'red' },
|
upload_rejected: { label: 'Rejected', color: 'red' },
|
||||||
achievement: { label: 'Achievement', color: 'gold' },
|
achievement: { label: 'Achievement', color: 'gold' },
|
||||||
system: { label: 'System', color: 'default' },
|
system: { label: 'System', color: 'default' },
|
||||||
shift_signup_confirmed: { label: 'Shift Signup', color: 'geekblue' },
|
|
||||||
shift_reminder: { label: 'Shift Reminder', color: 'purple' },
|
|
||||||
shift_cancelled: { label: 'Shift Cancelled', color: 'red' },
|
|
||||||
canvass_session_summary: { label: 'Canvass Summary', color: 'volcano' },
|
|
||||||
reengagement: { label: 'We Miss You', color: 'magenta' },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
|
|||||||
@ -1,37 +0,0 @@
|
|||||||
import type { UserRole } from '@/types/api';
|
|
||||||
|
|
||||||
/** Tag color for each role (Ant Design Tag color prop values) */
|
|
||||||
export const ROLE_COLORS: Record<string, string> = {
|
|
||||||
SUPER_ADMIN: 'red',
|
|
||||||
INFLUENCE_ADMIN: 'blue',
|
|
||||||
MAP_ADMIN: 'green',
|
|
||||||
BROADCAST_ADMIN: 'cyan',
|
|
||||||
CONTENT_ADMIN: 'geekblue',
|
|
||||||
MEDIA_ADMIN: 'purple',
|
|
||||||
PAYMENTS_ADMIN: 'gold',
|
|
||||||
EVENTS_ADMIN: 'magenta',
|
|
||||||
SOCIAL_ADMIN: 'volcano',
|
|
||||||
USER: 'default',
|
|
||||||
TEMP: 'orange',
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Role options for Select components (no "All" entry) */
|
|
||||||
export const ROLE_OPTIONS: { label: string; value: UserRole }[] = [
|
|
||||||
{ label: 'Super Admin', value: 'SUPER_ADMIN' },
|
|
||||||
{ label: 'Influence Admin', value: 'INFLUENCE_ADMIN' },
|
|
||||||
{ label: 'Map Admin', value: 'MAP_ADMIN' },
|
|
||||||
{ label: 'Broadcast Admin', value: 'BROADCAST_ADMIN' },
|
|
||||||
{ label: 'Content Admin', value: 'CONTENT_ADMIN' },
|
|
||||||
{ label: 'Media Admin', value: 'MEDIA_ADMIN' },
|
|
||||||
{ label: 'Payments Admin', value: 'PAYMENTS_ADMIN' },
|
|
||||||
{ label: 'Events Admin', value: 'EVENTS_ADMIN' },
|
|
||||||
{ label: 'Social Admin', value: 'SOCIAL_ADMIN' },
|
|
||||||
{ label: 'User', value: 'USER' },
|
|
||||||
{ label: 'Temp', value: 'TEMP' },
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Role options with a leading "All Roles" entry for filter dropdowns */
|
|
||||||
export const ROLE_FILTER_OPTIONS: { label: string; value: string }[] = [
|
|
||||||
{ label: 'All Roles', value: '' },
|
|
||||||
...ROLE_OPTIONS,
|
|
||||||
];
|
|
||||||
@ -1,117 +0,0 @@
|
|||||||
/**
|
|
||||||
* Monaco Editor wiki-link autocomplete provider.
|
|
||||||
*
|
|
||||||
* Registers a completion provider for markdown that triggers on `[`
|
|
||||||
* and provides file suggestions when the user types `[[`.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { FileNode } from '@/types/api';
|
|
||||||
|
|
||||||
interface FlatFile {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico']);
|
|
||||||
|
|
||||||
function isImage(name: string): boolean {
|
|
||||||
const dot = name.lastIndexOf('.');
|
|
||||||
return dot >= 0 && IMAGE_EXTENSIONS.has(name.substring(dot).toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenFiles(nodes: FileNode[]): FlatFile[] {
|
|
||||||
const out: FlatFile[] = [];
|
|
||||||
for (const n of nodes) {
|
|
||||||
if (n.isDirectory) {
|
|
||||||
if (n.children) out.push(...flattenFiles(n.children));
|
|
||||||
} else {
|
|
||||||
out.push({ path: n.path, name: n.name });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a wiki-link `[[` autocomplete provider for Monaco's markdown mode.
|
|
||||||
*
|
|
||||||
* @param monaco - The Monaco namespace (from onMount callback)
|
|
||||||
* @param getFileTree - Callback returning the current file tree
|
|
||||||
* @returns IDisposable to unregister the provider
|
|
||||||
*/
|
|
||||||
export function registerWikiLinkCompletion(
|
|
||||||
monaco: typeof import('monaco-editor'),
|
|
||||||
getFileTree: () => FileNode[],
|
|
||||||
): import('monaco-editor').IDisposable {
|
|
||||||
return monaco.languages.registerCompletionItemProvider('markdown', {
|
|
||||||
triggerCharacters: ['['],
|
|
||||||
provideCompletionItems(model, position) {
|
|
||||||
// Check that we're in a `[[` context
|
|
||||||
const lineContent = model.getLineContent(position.lineNumber);
|
|
||||||
const textBefore = lineContent.substring(0, position.column - 1);
|
|
||||||
|
|
||||||
// Must end with `[[` (possibly with partial text after it)
|
|
||||||
const wikiStart = textBefore.lastIndexOf('[[');
|
|
||||||
if (wikiStart < 0) return { suggestions: [] };
|
|
||||||
|
|
||||||
// Make sure there's no `]]` between wikiStart and cursor (not already closed)
|
|
||||||
const between = textBefore.substring(wikiStart + 2);
|
|
||||||
if (between.includes(']]')) return { suggestions: [] };
|
|
||||||
|
|
||||||
// Check if this is an image embed (![[)
|
|
||||||
const isEmbed = wikiStart > 0 && textBefore[wikiStart - 1] === '!';
|
|
||||||
|
|
||||||
// The partial query the user has typed after `[[`
|
|
||||||
const query = between.toLowerCase();
|
|
||||||
|
|
||||||
// Build the range from after `[[` to current cursor (or past any auto-closed `]]`)
|
|
||||||
const startCol = wikiStart + 3; // +2 for `[[`, +1 for 1-based
|
|
||||||
const textAfter = lineContent.substring(position.column - 1);
|
|
||||||
// If Monaco auto-closed brackets, there may be `]]` right after cursor — consume it
|
|
||||||
const closingMatch = textAfter.match(/^\]{1,2}/);
|
|
||||||
const extraClose = closingMatch ? closingMatch[0].length : 0;
|
|
||||||
const range = new monaco.Range(
|
|
||||||
position.lineNumber,
|
|
||||||
startCol,
|
|
||||||
position.lineNumber,
|
|
||||||
position.column + extraClose,
|
|
||||||
);
|
|
||||||
|
|
||||||
const files = flattenFiles(getFileTree());
|
|
||||||
const suggestions: import('monaco-editor').languages.CompletionItem[] = [];
|
|
||||||
|
|
||||||
for (const file of files) {
|
|
||||||
const fileIsImage = isImage(file.name);
|
|
||||||
|
|
||||||
// If user typed `![[`, prefer images; if `[[`, prefer docs
|
|
||||||
// But show all files regardless — just sort differently
|
|
||||||
const displayName = file.name.endsWith('.md')
|
|
||||||
? file.name.slice(0, -3)
|
|
||||||
: file.name;
|
|
||||||
|
|
||||||
// Filter by query
|
|
||||||
if (query && !displayName.toLowerCase().includes(query) && !file.path.toLowerCase().includes(query)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For wiki-links, we insert just the name (hook resolves the path)
|
|
||||||
const insertName = file.name.endsWith('.md')
|
|
||||||
? file.name.slice(0, -3)
|
|
||||||
: file.name;
|
|
||||||
|
|
||||||
suggestions.push({
|
|
||||||
label: displayName,
|
|
||||||
kind: fileIsImage
|
|
||||||
? monaco.languages.CompletionItemKind.File
|
|
||||||
: monaco.languages.CompletionItemKind.Reference,
|
|
||||||
detail: file.path,
|
|
||||||
insertText: insertName + ']]',
|
|
||||||
range,
|
|
||||||
// Sort: prioritize images for ![[, docs for [[
|
|
||||||
sortText: (isEmbed ? (fileIsImage ? '0' : '1') : (fileIsImage ? '1' : '0')) + displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return { suggestions };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -7,24 +7,12 @@ if [ "$NODE_ENV" = "production" ] && [ "$NODE_TLS_REJECT_UNAUTHORIZED" = "0" ];
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Wait for PostgreSQL to be ready before running migrations
|
|
||||||
echo "Waiting for database..."
|
|
||||||
MAX_WAIT=30
|
|
||||||
WAITED=0
|
|
||||||
until echo "SELECT 1" | npx prisma db execute --stdin --schema ./prisma/schema.prisma 2>/dev/null; do
|
|
||||||
sleep 2
|
|
||||||
WAITED=$((WAITED + 2))
|
|
||||||
if [ $WAITED -ge $MAX_WAIT ]; then
|
|
||||||
echo "FATAL: Database not available after ${MAX_WAIT}s"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
echo "Database ready (${WAITED}s)"
|
|
||||||
|
|
||||||
# Run migrations — fail hard on error (never fall back to db push, which causes drift)
|
|
||||||
echo "Running Prisma migrations..."
|
echo "Running Prisma migrations..."
|
||||||
npx prisma migrate deploy 2>&1
|
npx prisma migrate deploy 2>&1 || {
|
||||||
echo "Migrations complete."
|
echo "Migration failed, falling back to schema push..."
|
||||||
|
npx prisma db push --skip-generate 2>&1
|
||||||
|
}
|
||||||
|
echo "Database sync complete."
|
||||||
|
|
||||||
echo "Running database seed..."
|
echo "Running database seed..."
|
||||||
npx prisma db seed 2>&1
|
npx prisma db seed 2>&1
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
-- AlterEnum
|
|
||||||
-- Add operational notification types for shift/canvass/reengagement notifications
|
|
||||||
ALTER TYPE "NotificationType" ADD VALUE 'shift_signup_confirmed';
|
|
||||||
ALTER TYPE "NotificationType" ADD VALUE 'shift_reminder';
|
|
||||||
ALTER TYPE "NotificationType" ADD VALUE 'shift_cancelled';
|
|
||||||
ALTER TYPE "NotificationType" ADD VALUE 'canvass_session_summary';
|
|
||||||
ALTER TYPE "NotificationType" ADD VALUE 'reengagement';
|
|
||||||
|
|
||||||
-- AlterTable: Add campaign attribution to donation orders
|
|
||||||
ALTER TABLE "orders" ADD COLUMN "influence_campaign_id" TEXT;
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "idx_orders_influence_campaign" ON "orders"("influence_campaign_id");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "orders" ADD CONSTRAINT "orders_influence_campaign_id_fkey" FOREIGN KEY ("influence_campaign_id") REFERENCES "campaigns"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
|
||||||
@ -290,7 +290,6 @@ model Campaign {
|
|||||||
smsCampaigns SmsCampaign[] @relation("SmsCampaigns")
|
smsCampaigns SmsCampaign[] @relation("SmsCampaigns")
|
||||||
stories ImpactStory[] @relation("CampaignStories")
|
stories ImpactStory[] @relation("CampaignStories")
|
||||||
milestones CampaignMilestone[] @relation("CampaignMilestones")
|
milestones CampaignMilestone[] @relation("CampaignMilestones")
|
||||||
donationOrders Order[] @relation("CampaignDonations")
|
|
||||||
|
|
||||||
@@index([moderationStatus])
|
@@index([moderationStatus])
|
||||||
@@index([isUserGenerated])
|
@@index([isUserGenerated])
|
||||||
@ -1523,12 +1522,6 @@ enum NotificationType {
|
|||||||
shared_view_invite
|
shared_view_invite
|
||||||
shared_view_accepted
|
shared_view_accepted
|
||||||
calendar_event_invite
|
calendar_event_invite
|
||||||
// Operational notification types
|
|
||||||
shift_signup_confirmed
|
|
||||||
shift_reminder
|
|
||||||
shift_cancelled
|
|
||||||
canvass_session_summary
|
|
||||||
reengagement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@ -3479,8 +3472,6 @@ model Order {
|
|||||||
product Product? @relation(fields: [productId], references: [id])
|
product Product? @relation(fields: [productId], references: [id])
|
||||||
donationPageId String? @map("donation_page_id")
|
donationPageId String? @map("donation_page_id")
|
||||||
donationPage DonationPage? @relation("DonationPageOrders", fields: [donationPageId], references: [id], onDelete: SetNull)
|
donationPage DonationPage? @relation("DonationPageOrders", fields: [donationPageId], references: [id], onDelete: SetNull)
|
||||||
influenceCampaignId String? @map("influence_campaign_id")
|
|
||||||
influenceCampaign Campaign? @relation("CampaignDonations", fields: [influenceCampaignId], references: [id], onDelete: SetNull)
|
|
||||||
tickets Ticket[] @relation("TicketOrder")
|
tickets Ticket[] @relation("TicketOrder")
|
||||||
|
|
||||||
@@index([userId], map: "idx_orders_user")
|
@@index([userId], map: "idx_orders_user")
|
||||||
@ -3488,7 +3479,6 @@ model Order {
|
|||||||
@@index([status], map: "idx_orders_status")
|
@@index([status], map: "idx_orders_status")
|
||||||
@@index([type], map: "idx_orders_type")
|
@@index([type], map: "idx_orders_type")
|
||||||
@@index([donationPageId], map: "idx_orders_donation_page")
|
@@index([donationPageId], map: "idx_orders_donation_page")
|
||||||
@@index([influenceCampaignId], map: "idx_orders_influence_campaign")
|
|
||||||
@@map("orders")
|
@@map("orders")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,11 +33,7 @@ const envSchema = z.object({
|
|||||||
|
|
||||||
// Initial Super Admin (auto-created during database seeding)
|
// Initial Super Admin (auto-created during database seeding)
|
||||||
INITIAL_ADMIN_EMAIL: z.string().email().default('admin@cmlite.org'),
|
INITIAL_ADMIN_EMAIL: z.string().email().default('admin@cmlite.org'),
|
||||||
INITIAL_ADMIN_PASSWORD: z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS')
|
INITIAL_ADMIN_PASSWORD: z.string().min(12).default('REQUIRED_STRONG_PASSWORD_CHANGE_THIS'),
|
||||||
.refine(
|
|
||||||
(val) => val !== 'REQUIRED_STRONG_PASSWORD_CHANGE_THIS',
|
|
||||||
{ message: 'INITIAL_ADMIN_PASSWORD must be changed from the default placeholder value' },
|
|
||||||
),
|
|
||||||
|
|
||||||
// SMTP
|
// SMTP
|
||||||
SMTP_HOST: z.string().default('mailhog-changemaker'),
|
SMTP_HOST: z.string().default('mailhog-changemaker'),
|
||||||
|
|||||||
@ -28,13 +28,13 @@ const recurrenceRuleSchema = z.object({
|
|||||||
export const createItemSchema = z.object({
|
export const createItemSchema = z.object({
|
||||||
layerId: z.string().min(1),
|
layerId: z.string().min(1),
|
||||||
title: z.string().min(1).max(200),
|
title: z.string().min(1).max(200),
|
||||||
description: z.string().max(5000).optional(),
|
description: z.string().optional(),
|
||||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'),
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD'),
|
||||||
startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM'),
|
startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM'),
|
||||||
endTime: z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM'),
|
endTime: z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM'),
|
||||||
isAllDay: z.boolean().optional(),
|
isAllDay: z.boolean().optional(),
|
||||||
itemType: z.enum(['EVENT', 'TIME_BLOCK', 'REMINDER']),
|
itemType: z.enum(['EVENT', 'TIME_BLOCK', 'REMINDER']),
|
||||||
location: z.string().max(500).optional(),
|
location: z.string().optional(),
|
||||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).optional(),
|
||||||
visibility: z.enum(['PRIVATE', 'FRIENDS', 'PUBLIC']).optional(),
|
visibility: z.enum(['PRIVATE', 'FRIENDS', 'PUBLIC']).optional(),
|
||||||
busyStatus: z.enum(['BUSY', 'TENTATIVE', 'FREE']).optional(),
|
busyStatus: z.enum(['BUSY', 'TENTATIVE', 'FREE']).optional(),
|
||||||
@ -45,13 +45,13 @@ export const createItemSchema = z.object({
|
|||||||
|
|
||||||
export const updateItemSchema = z.object({
|
export const updateItemSchema = z.object({
|
||||||
title: z.string().min(1).max(200).optional(),
|
title: z.string().min(1).max(200).optional(),
|
||||||
description: z.string().max(5000).nullable().optional(),
|
description: z.string().nullable().optional(),
|
||||||
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD').optional(),
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be YYYY-MM-DD').optional(),
|
||||||
startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM').optional(),
|
startTime: z.string().regex(/^\d{2}:\d{2}$/, 'Start time must be HH:MM').optional(),
|
||||||
endTime: z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM').optional(),
|
endTime: z.string().regex(/^\d{2}:\d{2}$/, 'End time must be HH:MM').optional(),
|
||||||
isAllDay: z.boolean().optional(),
|
isAllDay: z.boolean().optional(),
|
||||||
itemType: z.enum(['EVENT', 'TIME_BLOCK', 'REMINDER']).optional(),
|
itemType: z.enum(['EVENT', 'TIME_BLOCK', 'REMINDER']).optional(),
|
||||||
location: z.string().max(500).nullable().optional(),
|
location: z.string().nullable().optional(),
|
||||||
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(),
|
color: z.string().regex(/^#[0-9a-fA-F]{6}$/).nullable().optional(),
|
||||||
visibility: z.enum(['PRIVATE', 'FRIENDS', 'PUBLIC']).nullable().optional(),
|
visibility: z.enum(['PRIVATE', 'FRIENDS', 'PUBLIC']).nullable().optional(),
|
||||||
busyStatus: z.enum(['BUSY', 'TENTATIVE', 'FREE']).optional(),
|
busyStatus: z.enum(['BUSY', 'TENTATIVE', 'FREE']).optional(),
|
||||||
|
|||||||
@ -484,14 +484,6 @@ export const feedService = {
|
|||||||
|
|
||||||
if (!exportToken) return null;
|
if (!exportToken) return null;
|
||||||
|
|
||||||
// Enforce 1-year expiry on export tokens (soft check — no DB column needed)
|
|
||||||
const TOKEN_MAX_AGE_MS = 365 * 24 * 60 * 60 * 1000; // 1 year
|
|
||||||
if (Date.now() - exportToken.createdAt.getTime() > TOKEN_MAX_AGE_MS) {
|
|
||||||
// Token expired — clean it up and return null
|
|
||||||
await prisma.calendarExportToken.delete({ where: { id: exportToken.id } }).catch(() => {});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const pastLimit = new Date(now);
|
const pastLimit = new Date(now);
|
||||||
pastLimit.setMonth(pastLimit.getMonth() - 1);
|
pastLimit.setMonth(pastLimit.getMonth() - 1);
|
||||||
|
|||||||
@ -32,8 +32,7 @@ function hashFilePath(path: string): string {
|
|||||||
function safeResolve(relativePath: string): string {
|
function safeResolve(relativePath: string): string {
|
||||||
const normalized = normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
const normalized = normalize(relativePath).replace(/^(\.\.(\/|\\|$))+/, '');
|
||||||
const resolved = pathResolve(DOCS_ROOT, normalized);
|
const resolved = pathResolve(DOCS_ROOT, normalized);
|
||||||
// Use DOCS_ROOT + sep to prevent prefix attacks (e.g., /mkdocs/docs-evil matching /mkdocs/docs)
|
if (!resolved.startsWith(DOCS_ROOT)) {
|
||||||
if (resolved !== DOCS_ROOT && !resolved.startsWith(DOCS_ROOT + '/')) {
|
|
||||||
throw new PathTraversalError();
|
throw new PathTraversalError();
|
||||||
}
|
}
|
||||||
return resolved;
|
return resolved;
|
||||||
|
|||||||
@ -21,10 +21,9 @@ router.use(requireNonTemp);
|
|||||||
|
|
||||||
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
|
// Removed duplicated isServiceOnline - now using shared utility from utils/health-check.ts
|
||||||
|
|
||||||
// GET /api/docs/status — check MkDocs and Code Server availability (content editors only)
|
// GET /api/docs/status — check MkDocs and Code Server availability
|
||||||
router.get(
|
router.get(
|
||||||
'/status',
|
'/status',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (_req: Request, res: Response, next: NextFunction) => {
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
const [mkdocsOnline, codeServerOnline, siteServerOnline] = await Promise.all([
|
||||||
@ -45,10 +44,9 @@ router.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /api/docs/config — return public-facing port numbers for iframe URLs (content editors only)
|
// GET /api/docs/config — return public-facing port numbers for iframe URLs
|
||||||
router.get(
|
router.get(
|
||||||
'/config',
|
'/config',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (_req: Request, res: Response, _next: NextFunction) => {
|
async (_req: Request, res: Response, _next: NextFunction) => {
|
||||||
res.json({
|
res.json({
|
||||||
codeServerPort: env.CODE_SERVER_PORT,
|
codeServerPort: env.CODE_SERVER_PORT,
|
||||||
@ -60,10 +58,9 @@ router.get(
|
|||||||
|
|
||||||
// --- MkDocs Config Endpoints ---
|
// --- MkDocs Config Endpoints ---
|
||||||
|
|
||||||
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content (content editors only)
|
// GET /api/docs/mkdocs-config — read raw mkdocs.yml content
|
||||||
router.get(
|
router.get(
|
||||||
'/mkdocs-config',
|
'/mkdocs-config',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (_req: Request, res: Response, next: NextFunction) => {
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const content = await mkdocsConfigService.readConfig();
|
const content = await mkdocsConfigService.readConfig();
|
||||||
@ -116,10 +113,9 @@ router.post(
|
|||||||
|
|
||||||
// --- Header Builder ---
|
// --- Header Builder ---
|
||||||
|
|
||||||
// GET /api/docs/header-config — read header nav bar config (content editors only)
|
// GET /api/docs/header-config — read header nav bar config
|
||||||
router.get(
|
router.get(
|
||||||
'/header-config',
|
'/header-config',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (_req: Request, res: Response, next: NextFunction) => {
|
async (_req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const config = await headerBuilderService.readConfig();
|
const config = await headerBuilderService.readConfig();
|
||||||
@ -209,10 +205,9 @@ router.post(
|
|||||||
|
|
||||||
// --- File Management Endpoints ---
|
// --- File Management Endpoints ---
|
||||||
|
|
||||||
// GET /api/docs/files — list file tree (content editors only)
|
// GET /api/docs/files — list file tree
|
||||||
router.get(
|
router.get(
|
||||||
'/files',
|
'/files',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
cm_docs_operations.inc({ operation: 'list' });
|
cm_docs_operations.inc({ operation: 'list' });
|
||||||
@ -228,10 +223,9 @@ router.get(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /api/docs/files/search — search files by name/path (content editors only)
|
// GET /api/docs/files/search — search files by name/path (for command palette)
|
||||||
router.get(
|
router.get(
|
||||||
'/files/search',
|
'/files/search',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
|
const search = String(req.query['search'] ?? req.query['q'] ?? '').trim();
|
||||||
@ -271,10 +265,9 @@ router.post(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// GET /api/docs/files/* — read file content (content editors only)
|
// GET /api/docs/files/* — read file content
|
||||||
router.get(
|
router.get(
|
||||||
'/files/*',
|
'/files/*',
|
||||||
requireRole(...CONTENT_ROLES),
|
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
cm_docs_operations.inc({ operation: 'read' });
|
cm_docs_operations.inc({ operation: 'read' });
|
||||||
|
|||||||
@ -3,11 +3,7 @@ import { z } from 'zod';
|
|||||||
export const headerNavItemSchema = z.object({
|
export const headerNavItemSchema = z.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
label: z.string().min(1).max(50),
|
label: z.string().min(1).max(50),
|
||||||
path: z.string().min(1).max(500)
|
path: z.string().min(1).max(500),
|
||||||
.refine(
|
|
||||||
(v) => !/^(javascript|data|vbscript):/i.test(v),
|
|
||||||
'Dangerous URL scheme not allowed',
|
|
||||||
),
|
|
||||||
icon: z.string().max(50).optional(),
|
icon: z.string().max(50).optional(),
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
order: z.number().int().min(0),
|
order: z.number().int().min(0),
|
||||||
|
|||||||
@ -589,12 +589,6 @@ class HeaderBuilderService {
|
|||||||
<div class="cm-header-nav__links">
|
<div class="cm-header-nav__links">
|
||||||
<div class="cm-header-nav__links-inner">
|
<div class="cm-header-nav__links-inner">
|
||||||
${desktopLinks}
|
${desktopLinks}
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -627,20 +621,6 @@ class HeaderBuilderService {
|
|||||||
</div>
|
</div>
|
||||||
<div class="cm-header-nav__mobile-links">
|
<div class="cm-header-nav__mobile-links">
|
||||||
${mobileLinks}
|
${mobileLinks}
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -744,74 +724,6 @@ class HeaderBuilderService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// This avoids reliance on the ~ sibling combinator (fragile with template blocks).
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -1054,127 +966,8 @@ class HeaderBuilderService {
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
{% if config.theme.palette %}
|
|
||||||
{% if not config.theme.palette is mapping %}
|
|
||||||
{% include "partials/palette.html" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if "material/search" in config.plugins %}
|
|
||||||
{% include "partials/search.html" %}
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block tabs %}{% endblock %}
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,6 @@ import { prisma } from '../../../config/database';
|
|||||||
import { AppError } from '../../../middleware/error-handler';
|
import { AppError } from '../../../middleware/error-handler';
|
||||||
import { emailQueueService } from '../../../services/email-queue.service';
|
import { emailQueueService } from '../../../services/email-queue.service';
|
||||||
import { recordCampaignEmail } from '../../../utils/metrics';
|
import { recordCampaignEmail } from '../../../utils/metrics';
|
||||||
import { recordCrmActivity } from '../../../utils/crm-activity';
|
|
||||||
import { groupService } from '../../social/group.service';
|
import { groupService } from '../../social/group.service';
|
||||||
import { achievementsService } from '../../social/achievements.service';
|
import { achievementsService } from '../../social/achievements.service';
|
||||||
import type { SendCampaignEmailInput, TrackMailtoInput, ListCampaignEmailsInput } from './campaign-emails.schemas';
|
import type { SendCampaignEmailInput, TrackMailtoInput, ListCampaignEmailsInput } from './campaign-emails.schemas';
|
||||||
@ -90,14 +89,6 @@ export const campaignEmailsService = {
|
|||||||
|
|
||||||
recordCampaignEmail(campaign.id);
|
recordCampaignEmail(campaign.id);
|
||||||
|
|
||||||
// CRM activity (fire-and-forget)
|
|
||||||
recordCrmActivity({
|
|
||||||
email: data.userEmail,
|
|
||||||
activityType: 'EMAIL_SENT',
|
|
||||||
title: `Sent campaign email: ${campaign.title}`,
|
|
||||||
metadata: { campaignId: campaign.id, campaignSlug: campaign.slug, recipientEmail: data.recipientEmail, emailMethod: data.emailMethod },
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
// Social group sync (fire-and-forget)
|
// Social group sync (fire-and-forget)
|
||||||
groupService.syncCampaignTeam(campaign.id).catch(() => {});
|
groupService.syncCampaignTeam(campaign.id).catch(() => {});
|
||||||
|
|
||||||
|
|||||||
@ -56,41 +56,6 @@ const campaignSelect = {
|
|||||||
},
|
},
|
||||||
} satisfies Prisma.CampaignSelect;
|
} satisfies Prisma.CampaignSelect;
|
||||||
|
|
||||||
/** Public-facing select — strips admin-only fields (emails, internal IDs, moderation notes) */
|
|
||||||
const publicCampaignSelect = {
|
|
||||||
id: true,
|
|
||||||
slug: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
emailSubject: true,
|
|
||||||
emailBody: true,
|
|
||||||
callToAction: true,
|
|
||||||
coverPhoto: true,
|
|
||||||
coverVideoId: true,
|
|
||||||
status: true,
|
|
||||||
allowSmtpEmail: true,
|
|
||||||
allowMailtoLink: true,
|
|
||||||
collectUserInfo: true,
|
|
||||||
showEmailCount: true,
|
|
||||||
showCallCount: true,
|
|
||||||
allowEmailEditing: true,
|
|
||||||
allowCustomRecipients: true,
|
|
||||||
showResponseWall: true,
|
|
||||||
highlightCampaign: true,
|
|
||||||
targetGovernmentLevels: true,
|
|
||||||
createdByUserName: true,
|
|
||||||
isUserGenerated: true,
|
|
||||||
moderationStatus: true,
|
|
||||||
createdAt: true,
|
|
||||||
updatedAt: true,
|
|
||||||
_count: {
|
|
||||||
select: {
|
|
||||||
emails: true,
|
|
||||||
responses: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies Prisma.CampaignSelect;
|
|
||||||
|
|
||||||
function generateSlug(title: string): string {
|
function generateSlug(title: string): string {
|
||||||
return title
|
return title
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@ -259,7 +224,7 @@ export const campaignsService = {
|
|||||||
async findActiveCampaigns() {
|
async findActiveCampaigns() {
|
||||||
return prisma.campaign.findMany({
|
return prisma.campaign.findMany({
|
||||||
where: { status: 'ACTIVE' },
|
where: { status: 'ACTIVE' },
|
||||||
select: publicCampaignSelect,
|
select: campaignSelect,
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ highlightCampaign: 'desc' },
|
{ highlightCampaign: 'desc' },
|
||||||
{ createdAt: 'desc' },
|
{ createdAt: 'desc' },
|
||||||
@ -270,7 +235,7 @@ export const campaignsService = {
|
|||||||
async findBySlugPublic(slug: string) {
|
async findBySlugPublic(slug: string) {
|
||||||
const campaign = await prisma.campaign.findUnique({
|
const campaign = await prisma.campaign.findUnique({
|
||||||
where: { slug },
|
where: { slug },
|
||||||
select: publicCampaignSelect,
|
select: campaignSelect,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!campaign) {
|
if (!campaign) {
|
||||||
|
|||||||
@ -15,8 +15,7 @@ router.post(
|
|||||||
'/webhook',
|
'/webhook',
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
// Accept secret from header (preferred) or query param (legacy fallback)
|
const secret = req.query.secret as string;
|
||||||
const secret = (req.headers['x-webhook-secret'] as string) || (req.query.secret as string);
|
|
||||||
if (!env.LISTMONK_WEBHOOK_SECRET || secret !== env.LISTMONK_WEBHOOK_SECRET) {
|
if (!env.LISTMONK_WEBHOOK_SECRET || secret !== env.LISTMONK_WEBHOOK_SECRET) {
|
||||||
res.status(403).json({ error: 'Invalid webhook secret' });
|
res.status(403).json({ error: 'Invalid webhook secret' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import { AppError } from '../../../middleware/error-handler';
|
|||||||
import { logger } from '../../../utils/logger';
|
import { logger } from '../../../utils/logger';
|
||||||
import { recordLocationQuery } from '../../../utils/metrics';
|
import { recordLocationQuery } from '../../../utils/metrics';
|
||||||
import { isPointInPolygon, parseGeoJsonPolygon } from '../../../utils/spatial';
|
import { isPointInPolygon, parseGeoJsonPolygon } from '../../../utils/spatial';
|
||||||
import { recordCrmActivity } from '../../../utils/crm-activity';
|
|
||||||
import { calculateWalkingRoute } from './canvass-route.service';
|
import { calculateWalkingRoute } from './canvass-route.service';
|
||||||
import { recordCanvassVisit, setActiveCanvassSessions } from '../../../utils/metrics';
|
import { recordCanvassVisit, setActiveCanvassSessions } from '../../../utils/metrics';
|
||||||
import { notificationQueueService } from '../../../services/notification-queue.service';
|
import { notificationQueueService } from '../../../services/notification-queue.service';
|
||||||
@ -654,21 +653,6 @@ export const canvassService = {
|
|||||||
|
|
||||||
recordCanvassVisit(data.outcome);
|
recordCanvassVisit(data.outcome);
|
||||||
|
|
||||||
// CRM activity via ContactAddress lookup (fire-and-forget)
|
|
||||||
prisma.contactAddress.findFirst({
|
|
||||||
where: { addressId: data.addressId },
|
|
||||||
select: { contactId: true },
|
|
||||||
}).then((ca) => {
|
|
||||||
if (ca) {
|
|
||||||
recordCrmActivity({
|
|
||||||
contactId: ca.contactId,
|
|
||||||
activityType: 'CANVASS_VISIT',
|
|
||||||
title: `Canvass visit: ${data.outcome}`,
|
|
||||||
metadata: { addressId: data.addressId, outcome: data.outcome, visitId: visit.id },
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
// Achievement check (fire-and-forget)
|
// Achievement check (fire-and-forget)
|
||||||
achievementsService.checkAndUnlock(userId, ['canvass']).catch(() => {});
|
achievementsService.checkAndUnlock(userId, ['canvass']).catch(() => {});
|
||||||
|
|
||||||
|
|||||||
@ -28,16 +28,6 @@ router.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get series shift count
|
|
||||||
router.get('/:id/count', async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const count = await ShiftSeriesService.getShiftCount(req.params.id as string);
|
|
||||||
res.json({ count });
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get series
|
// Get series
|
||||||
router.get('/:id', async (req, res, next) => {
|
router.get('/:id', async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -117,15 +117,6 @@ export class ShiftSeriesService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get count of non-exception shifts in a series
|
|
||||||
*/
|
|
||||||
static async getShiftCount(seriesId: string): Promise<number> {
|
|
||||||
return prisma.shift.count({
|
|
||||||
where: { seriesId, isException: false },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get series with all its shifts
|
* Get series with all its shifts
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export async function commentsRoutes(fastify: FastifyInstance) {
|
|||||||
user: comment.user
|
user: comment.user
|
||||||
? {
|
? {
|
||||||
id: comment.user.id,
|
id: comment.user.id,
|
||||||
name: comment.user.name || 'Anonymous',
|
name: comment.user.name || comment.user.email,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
}));
|
}));
|
||||||
@ -229,7 +229,7 @@ export async function commentsRoutes(fastify: FastifyInstance) {
|
|||||||
user: newComment.user
|
user: newComment.user
|
||||||
? {
|
? {
|
||||||
id: newComment.user.id,
|
id: newComment.user.id,
|
||||||
name: newComment.user.name || 'Anonymous',
|
name: newComment.user.name || newComment.user.email,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
};
|
};
|
||||||
@ -253,7 +253,7 @@ export async function commentsRoutes(fastify: FastifyInstance) {
|
|||||||
select: { filename: true },
|
select: { filename: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const commenterName = newComment.user?.name || 'Someone';
|
const commenterName = newComment.user?.name || newComment.user?.email || 'Someone';
|
||||||
const contentPreview = content.trim().length > 80
|
const contentPreview = content.trim().length > 80
|
||||||
? content.trim().substring(0, 80) + '...'
|
? content.trim().substring(0, 80) + '...'
|
||||||
: content.trim();
|
: content.trim();
|
||||||
|
|||||||
@ -186,23 +186,6 @@ export async function videoTrackingRoutes(fastify: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Validate each event's data against per-type schemas before processing
|
|
||||||
for (const event of events) {
|
|
||||||
switch (event.type) {
|
|
||||||
case 'view':
|
|
||||||
recordViewSchema.parse(event.data);
|
|
||||||
break;
|
|
||||||
case 'event':
|
|
||||||
recordEventSchema.parse(event.data);
|
|
||||||
break;
|
|
||||||
case 'heartbeat':
|
|
||||||
updateWatchTimeSchema.parse(event.data);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return reply.code(400).send({ message: `Unknown event type: ${event.type}` });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
events.map(async (event) => {
|
events.map(async (event) => {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export const donationsService = {
|
|||||||
donationPageId?: string,
|
donationPageId?: string,
|
||||||
donationPageSlug?: string,
|
donationPageSlug?: string,
|
||||||
donationPageTitle?: string,
|
donationPageTitle?: string,
|
||||||
campaignId?: string,
|
|
||||||
) {
|
) {
|
||||||
const settings = await paymentSettingsService.get();
|
const settings = await paymentSettingsService.get();
|
||||||
if (!settings.enableDonations) throw new Error('Donations are currently disabled');
|
if (!settings.enableDonations) throw new Error('Donations are currently disabled');
|
||||||
@ -56,7 +55,6 @@ export const donationsService = {
|
|||||||
message: message || '',
|
message: message || '',
|
||||||
isAnonymous: isAnonymous ? 'true' : 'false',
|
isAnonymous: isAnonymous ? 'true' : 'false',
|
||||||
donationPageId: donationPageId || '',
|
donationPageId: donationPageId || '',
|
||||||
campaignId: campaignId || '',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -72,7 +70,6 @@ export const donationsService = {
|
|||||||
donorMessage: message || null,
|
donorMessage: message || null,
|
||||||
isAnonymous: isAnonymous || false,
|
isAnonymous: isAnonymous || false,
|
||||||
donationPageId: donationPageId || null,
|
donationPageId: donationPageId || null,
|
||||||
influenceCampaignId: campaignId || null,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -125,17 +125,13 @@ router.post(
|
|||||||
validate(createDonationCheckoutSchema),
|
validate(createDonationCheckoutSchema),
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const { amountCents, email, name, message, isAnonymous, campaignId } = req.body;
|
const { amountCents, email, name, message, isAnonymous } = req.body;
|
||||||
const result = await donationsService.createDonationCheckout(
|
const result = await donationsService.createDonationCheckout(
|
||||||
amountCents,
|
amountCents,
|
||||||
email,
|
email,
|
||||||
name,
|
name,
|
||||||
message,
|
message,
|
||||||
isAnonymous,
|
isAnonymous,
|
||||||
undefined, // donationPageId
|
|
||||||
undefined, // donationPageSlug
|
|
||||||
undefined, // donationPageTitle
|
|
||||||
campaignId,
|
|
||||||
);
|
);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -89,7 +89,6 @@ export const createDonationCheckoutSchema = z.object({
|
|||||||
name: z.string().max(200).optional(),
|
name: z.string().max(200).optional(),
|
||||||
message: z.string().max(2000).optional(),
|
message: z.string().max(2000).optional(),
|
||||||
isAnonymous: z.boolean().optional(),
|
isAnonymous: z.boolean().optional(),
|
||||||
campaignId: z.string().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- Refund ---
|
// --- Refund ---
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import Stripe from 'stripe';
|
|||||||
import { prisma } from '../../config/database';
|
import { prisma } from '../../config/database';
|
||||||
import { getStripe, getWebhookSecret } from '../../services/stripe.client';
|
import { getStripe, getWebhookSecret } from '../../services/stripe.client';
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
import { recordCrmActivity } from '../../utils/crm-activity';
|
|
||||||
import { paymentEmailService } from './payment-email.service';
|
import { paymentEmailService } from './payment-email.service';
|
||||||
import { listmonkEventSyncService } from '../../services/listmonk-event-sync.service';
|
import { listmonkEventSyncService } from '../../services/listmonk-event-sync.service';
|
||||||
|
|
||||||
@ -215,16 +214,6 @@ export const webhookService = {
|
|||||||
orderId: updatedOrder.id,
|
orderId: updatedOrder.id,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRM activity (fire-and-forget)
|
|
||||||
if (updatedOrder.buyerEmail) {
|
|
||||||
recordCrmActivity({
|
|
||||||
email: updatedOrder.buyerEmail,
|
|
||||||
activityType: 'PURCHASE',
|
|
||||||
title: `Purchased: ${updatedOrder.product?.title || 'Product'}`,
|
|
||||||
metadata: { orderId: updatedOrder.id, productId: updatedOrder.product ? order.productId : null, amountCents: updatedOrder.amountCAD },
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -242,9 +231,8 @@ export const webhookService = {
|
|||||||
? session.payment_intent
|
? session.payment_intent
|
||||||
: (session.payment_intent as { id: string } | null)?.id || null;
|
: (session.payment_intent as { id: string } | null)?.id || null;
|
||||||
|
|
||||||
// Link to donation page and/or campaign if metadata contains them
|
// Link to donation page if metadata contains donationPageId (from page-specific checkout)
|
||||||
const donationPageId = session.metadata?.donationPageId || null;
|
const donationPageId = session.metadata?.donationPageId || null;
|
||||||
const campaignId = session.metadata?.campaignId || null;
|
|
||||||
const updateData: Record<string, unknown> = {
|
const updateData: Record<string, unknown> = {
|
||||||
status: 'COMPLETED',
|
status: 'COMPLETED',
|
||||||
stripePaymentIntentId: paymentIntentId,
|
stripePaymentIntentId: paymentIntentId,
|
||||||
@ -253,9 +241,6 @@ export const webhookService = {
|
|||||||
if (donationPageId && !order.donationPageId) {
|
if (donationPageId && !order.donationPageId) {
|
||||||
updateData.donationPageId = donationPageId;
|
updateData.donationPageId = donationPageId;
|
||||||
}
|
}
|
||||||
if (campaignId && !order.influenceCampaignId) {
|
|
||||||
updateData.influenceCampaignId = campaignId;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.order.update({
|
await prisma.order.update({
|
||||||
where: { id: order.id },
|
where: { id: order.id },
|
||||||
@ -289,16 +274,6 @@ export const webhookService = {
|
|||||||
orderId: order.id,
|
orderId: order.id,
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRM activity (fire-and-forget)
|
|
||||||
if (order.buyerEmail) {
|
|
||||||
recordCrmActivity({
|
|
||||||
email: order.buyerEmail,
|
|
||||||
activityType: 'DONATION',
|
|
||||||
title: `Donation: $${(order.amountCAD / 100).toFixed(2)}`,
|
|
||||||
metadata: { orderId: order.id, amountCents: order.amountCAD },
|
|
||||||
}).catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
async handleInvoicePaid(invoice: Stripe.Invoice) {
|
async handleInvoicePaid(invoice: Stripe.Invoice) {
|
||||||
|
|||||||
@ -130,20 +130,14 @@ router.post('/test-connection', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate URL format and protocol
|
// Validate URL format
|
||||||
let parsedUrl: URL;
|
|
||||||
try {
|
try {
|
||||||
parsedUrl = new URL(url);
|
new URL(url);
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({ error: { message: 'Invalid URL format', code: 'VALIDATION_ERROR' } });
|
res.status(400).json({ error: { message: 'Invalid URL format', code: 'VALIDATION_ERROR' } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
|
|
||||||
res.status(400).json({ error: { message: 'Only http and https URLs are allowed', code: 'VALIDATION_ERROR' } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await TermuxClient.testConnection(url, apiKey);
|
const result = await TermuxClient.testConnection(url, apiKey);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -179,14 +173,10 @@ router.post('/save-config', async (req, res) => {
|
|||||||
if (typeof smsTailscaleDeviceId === 'string') update.smsTailscaleDeviceId = smsTailscaleDeviceId;
|
if (typeof smsTailscaleDeviceId === 'string') update.smsTailscaleDeviceId = smsTailscaleDeviceId;
|
||||||
if (typeof smsTailscaleDeviceName === 'string') update.smsTailscaleDeviceName = smsTailscaleDeviceName;
|
if (typeof smsTailscaleDeviceName === 'string') update.smsTailscaleDeviceName = smsTailscaleDeviceName;
|
||||||
|
|
||||||
// Validate URL format and protocol if provided
|
// Validate URL format if provided
|
||||||
if (typeof smsTermuxApiUrl === 'string' && smsTermuxApiUrl) {
|
if (typeof smsTermuxApiUrl === 'string' && smsTermuxApiUrl) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(smsTermuxApiUrl);
|
new URL(smsTermuxApiUrl);
|
||||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
||||||
res.status(400).json({ error: { message: 'Only http and https URLs are allowed', code: 'VALIDATION_ERROR' } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({ error: { message: 'Invalid Termux API URL format', code: 'VALIDATION_ERROR' } });
|
res.status(400).json({ error: { message: 'Invalid Termux API URL format', code: 'VALIDATION_ERROR' } });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -22,20 +22,11 @@ router.get('/', async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /api/social/groups/:id — group detail with members (membership required) */
|
/** GET /api/social/groups/:id — group detail with members */
|
||||||
router.get('/:id', async (req: Request, res: Response) => {
|
router.get('/:id', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const groupId = req.params.id as string;
|
const groupId = req.params.id as string;
|
||||||
const userId = req.user!.id;
|
|
||||||
const result = await groupService.getGroupDetail(groupId);
|
const result = await groupService.getGroupDetail(groupId);
|
||||||
|
|
||||||
// Verify the requesting user is a member of this group
|
|
||||||
const isMember = result.members.some((m: any) => m.userId === userId);
|
|
||||||
if (!isMember) {
|
|
||||||
res.status(404).json({ error: { message: 'Group not found', code: 'NOT_FOUND' } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
res.status(err.statusCode || 500).json({ error: { message: err.message } });
|
res.status(err.statusCode || 500).json({ error: { message: err.message } });
|
||||||
|
|||||||
@ -19,12 +19,6 @@ const TYPE_TO_PREF: Record<string, string> = {
|
|||||||
shared_view_invite: 'enableFriendRequests',
|
shared_view_invite: 'enableFriendRequests',
|
||||||
shared_view_accepted: 'enableFriendRequests',
|
shared_view_accepted: 'enableFriendRequests',
|
||||||
calendar_event_invite: 'enableFriendRequests',
|
calendar_event_invite: 'enableFriendRequests',
|
||||||
// Operational notification types
|
|
||||||
shift_signup_confirmed: 'enableSystemUpdates',
|
|
||||||
shift_reminder: 'enableSystemUpdates',
|
|
||||||
shift_cancelled: 'enableSystemUpdates',
|
|
||||||
canvass_session_summary: 'enableSystemUpdates',
|
|
||||||
reengagement: 'enableSystemUpdates',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const notificationService = {
|
export const notificationService = {
|
||||||
|
|||||||
@ -7,19 +7,10 @@ import { blockService } from './block.service';
|
|||||||
import { messagingService } from './messaging.service';
|
import { messagingService } from './messaging.service';
|
||||||
import { checkSocialEnabled } from './social.middleware';
|
import { checkSocialEnabled } from './social.middleware';
|
||||||
|
|
||||||
/** Own profile — includes email */
|
|
||||||
const OWN_PROFILE_SELECT = {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
role: true,
|
|
||||||
createdAt: true,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/** Other users' profiles — email stripped to prevent harvesting */
|
|
||||||
const PROFILE_SELECT = {
|
const PROFILE_SELECT = {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
email: true,
|
||||||
role: true,
|
role: true,
|
||||||
createdAt: true,
|
createdAt: true,
|
||||||
} as const;
|
} as const;
|
||||||
@ -34,7 +25,7 @@ router.get('/me', async (req: Request, res: Response) => {
|
|||||||
const userId = req.user!.id;
|
const userId = req.user!.id;
|
||||||
|
|
||||||
const [user, friendCount, pendingCount, privacy] = await Promise.all([
|
const [user, friendCount, pendingCount, privacy] = await Promise.all([
|
||||||
prisma.user.findUnique({ where: { id: userId }, select: OWN_PROFILE_SELECT }),
|
prisma.user.findUnique({ where: { id: userId }, select: PROFILE_SELECT }),
|
||||||
prisma.friendship.count({
|
prisma.friendship.count({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [
|
||||||
|
|||||||
@ -1,15 +1,9 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { z } from 'zod';
|
|
||||||
import { socialAdminService } from './social-admin.service';
|
import { socialAdminService } from './social-admin.service';
|
||||||
import { requireRole } from '../../middleware/rbac.middleware';
|
|
||||||
import { SOCIAL_ROLES } from '../../utils/roles';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Self-contained auth guard (defense-in-depth — parent also applies requireRole)
|
|
||||||
router.use(requireRole(...SOCIAL_ROLES));
|
|
||||||
|
|
||||||
/** GET /api/social/admin/stats — Dashboard overview */
|
/** GET /api/social/admin/stats — Dashboard overview */
|
||||||
router.get('/stats', async (_req, res) => {
|
router.get('/stats', async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -74,19 +68,14 @@ router.post('/blocks/:id/remove', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const achievementSchema = z.object({
|
|
||||||
userId: z.string().min(1).max(100),
|
|
||||||
achievementId: z.string().min(1).max(100),
|
|
||||||
});
|
|
||||||
|
|
||||||
/** POST /api/social/admin/achievements/grant — Grant achievement */
|
/** POST /api/social/admin/achievements/grant — Grant achievement */
|
||||||
router.post('/achievements/grant', async (req, res) => {
|
router.post('/achievements/grant', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const parsed = achievementSchema.safeParse(req.body);
|
const { userId, achievementId } = req.body;
|
||||||
if (!parsed.success) {
|
if (!userId || !achievementId) {
|
||||||
return res.status(400).json({ error: 'Valid userId and achievementId are required' });
|
return res.status(400).json({ error: 'userId and achievementId are required' });
|
||||||
}
|
}
|
||||||
const result = await socialAdminService.grantAchievement(parsed.data.userId, parsed.data.achievementId);
|
const result = await socialAdminService.grantAchievement(userId, achievementId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
||||||
@ -98,11 +87,11 @@ router.post('/achievements/grant', async (req, res) => {
|
|||||||
/** POST /api/social/admin/achievements/revoke — Revoke achievement */
|
/** POST /api/social/admin/achievements/revoke — Revoke achievement */
|
||||||
router.post('/achievements/revoke', async (req, res) => {
|
router.post('/achievements/revoke', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const parsed = achievementSchema.safeParse(req.body);
|
const { userId, achievementId } = req.body;
|
||||||
if (!parsed.success) {
|
if (!userId || !achievementId) {
|
||||||
return res.status(400).json({ error: 'Valid userId and achievementId are required' });
|
return res.status(400).json({ error: 'userId and achievementId are required' });
|
||||||
}
|
}
|
||||||
await socialAdminService.revokeAchievement(parsed.data.userId, parsed.data.achievementId);
|
await socialAdminService.revokeAchievement(userId, achievementId);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
const statusCode = (err as { statusCode?: number }).statusCode ?? 500;
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export async function checkSocialEnabled(req: Request, res: Response, next: Next
|
|||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
} catch {
|
} catch {
|
||||||
// Fail closed — if we can't check the feature flag, deny access
|
next();
|
||||||
res.status(503).json({ error: { message: 'Service temporarily unavailable', code: 'SERVICE_UNAVAILABLE' } });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,8 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { checkSocialEnabled } from './social.middleware';
|
import { checkSocialEnabled } from './social.middleware';
|
||||||
import { requireRole } from '../../middleware/rbac.middleware';
|
|
||||||
import { SOCIAL_ROLES } from '../../utils/roles';
|
|
||||||
import { sseService } from './sse.service';
|
import { sseService } from './sse.service';
|
||||||
import { presenceService } from './presence.service';
|
import { presenceService } from './presence.service';
|
||||||
|
|
||||||
const MAX_SSE_CONNECTIONS_PER_USER = 5;
|
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(checkSocialEnabled);
|
router.use(checkSocialEnabled);
|
||||||
@ -15,13 +11,6 @@ router.use(checkSocialEnabled);
|
|||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
const userId = req.user!.id;
|
const userId = req.user!.id;
|
||||||
|
|
||||||
// Enforce per-user connection limit to prevent resource exhaustion
|
|
||||||
const existingCount = sseService.getConnectionCountForUser?.(userId) ?? 0;
|
|
||||||
if (existingCount >= MAX_SSE_CONNECTIONS_PER_USER) {
|
|
||||||
res.status(429).json({ error: { message: 'Too many SSE connections', code: 'TOO_MANY_CONNECTIONS' } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set SSE headers
|
// Set SSE headers
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
@ -59,8 +48,8 @@ router.get('/online-friends', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** GET /api/social/sse/status — SSE service status (admin only) */
|
/** GET /api/social/sse/status — SSE service status */
|
||||||
router.get('/status', requireRole(...SOCIAL_ROLES), (_req, res) => {
|
router.get('/status', (_req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
connections: sseService.getConnectionCount(),
|
connections: sseService.getConnectionCount(),
|
||||||
connectedUsers: sseService.getConnectedUserIds().length,
|
connectedUsers: sseService.getConnectedUserIds().length,
|
||||||
|
|||||||
@ -117,11 +117,6 @@ class SSEService {
|
|||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get connection count for a specific user */
|
|
||||||
getConnectionCountForUser(userId: string): number {
|
|
||||||
return this.clients.get(userId)?.length ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Close all connections (graceful shutdown) */
|
/** Close all connections (graceful shutdown) */
|
||||||
closeAll() {
|
closeAll() {
|
||||||
this.stopHeartbeat();
|
this.stopHeartbeat();
|
||||||
|
|||||||
@ -1,12 +1,13 @@
|
|||||||
import { Router, Request, Response, NextFunction } from 'express';
|
import { Router, Request, Response, NextFunction } from 'express';
|
||||||
import { authenticate } from '../../middleware/auth.middleware';
|
import { authenticate } from '../../middleware/auth.middleware';
|
||||||
import { requireRole } from '../../middleware/rbac.middleware';
|
import { requireRole } from '../../middleware/rbac.middleware';
|
||||||
|
import { ADMIN_ROLES } from '../../utils/roles';
|
||||||
import { userProvisioningService } from '../../services/user-provisioning/provisioning.service';
|
import { userProvisioningService } from '../../services/user-provisioning/provisioning.service';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.use(authenticate);
|
router.use(authenticate);
|
||||||
router.use(requireRole('SUPER_ADMIN'));
|
router.use(requireRole(...ADMIN_ROLES));
|
||||||
|
|
||||||
// POST /api/users/provisioning/sync — bulk sync all users (static route BEFORE :id params)
|
// POST /api/users/provisioning/sync — bulk sync all users (static route BEFORE :id params)
|
||||||
router.post(
|
router.post(
|
||||||
|
|||||||
@ -62,6 +62,7 @@ import { notificationQueueService } from './services/notification-queue.service'
|
|||||||
import { geocodeQueueService } from './services/geocode-queue.service';
|
import { geocodeQueueService } from './services/geocode-queue.service';
|
||||||
import { startProxy, stopProxy } from './services/listmonk-proxy.service';
|
import { startProxy, stopProxy } from './services/listmonk-proxy.service';
|
||||||
import { pagesService } from './modules/pages/pages.service';
|
import { pagesService } from './modules/pages/pages.service';
|
||||||
|
import { listmonkSyncService } from './services/listmonk-sync.service';
|
||||||
import { canvassService } from './modules/map/canvass/canvass.service';
|
import { canvassService } from './modules/map/canvass/canvass.service';
|
||||||
import { trackingService } from './modules/map/tracking/tracking.service';
|
import { trackingService } from './modules/map/tracking/tracking.service';
|
||||||
import { verificationTokenService } from './services/verification-token.service';
|
import { verificationTokenService } from './services/verification-token.service';
|
||||||
@ -114,7 +115,6 @@ import { presenceService } from './modules/social/presence.service';
|
|||||||
import { upgradeService } from './modules/upgrade/upgrade.service';
|
import { upgradeService } from './modules/upgrade/upgrade.service';
|
||||||
import { autoUpgradeService } from './services/auto-upgrade.service';
|
import { autoUpgradeService } from './services/auto-upgrade.service';
|
||||||
import { calendarFeedQueueService } from './services/calendar-feed-queue.service';
|
import { calendarFeedQueueService } from './services/calendar-feed-queue.service';
|
||||||
import { scheduledJobsQueueService } from './services/scheduled-jobs-queue.service';
|
|
||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import { docsCollabService } from './modules/docs/docs-collab.service';
|
import { docsCollabService } from './modules/docs/docs-collab.service';
|
||||||
|
|
||||||
@ -324,7 +324,6 @@ async function start() {
|
|||||||
notificationQueueService.startWorker();
|
notificationQueueService.startWorker();
|
||||||
geocodeQueueService.startWorker();
|
geocodeQueueService.startWorker();
|
||||||
calendarFeedQueueService.startWorker();
|
calendarFeedQueueService.startWorker();
|
||||||
scheduledJobsQueueService.startWorker();
|
|
||||||
startProxy();
|
startProxy();
|
||||||
|
|
||||||
// Load SMS config from DB (env fallback for empty fields)
|
// Load SMS config from DB (env fallback for empty fields)
|
||||||
@ -342,15 +341,47 @@ async function start() {
|
|||||||
logger.info('SMS integration enabled (Termux API)');
|
logger.info('SMS integration enabled (Termux API)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// One-time startup calls (recurring runs handled by scheduled-jobs queue)
|
// Clean expired verification/reset tokens on startup + hourly
|
||||||
verificationTokenService.cleanupExpiredTokens().catch(() => {});
|
verificationTokenService.cleanupExpiredTokens().catch(() => {});
|
||||||
passwordResetTokenService.cleanupExpiredTokens().catch(() => {});
|
passwordResetTokenService.cleanupExpiredTokens().catch(() => {});
|
||||||
|
setInterval(() => {
|
||||||
|
verificationTokenService.cleanupExpiredTokens().catch(() => {});
|
||||||
|
passwordResetTokenService.cleanupExpiredTokens().catch(() => {});
|
||||||
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Close abandoned canvass sessions on startup + hourly
|
||||||
canvassService.closeAbandonedSessions().catch(() => {});
|
canvassService.closeAbandonedSessions().catch(() => {});
|
||||||
|
setInterval(() => {
|
||||||
|
canvassService.closeAbandonedSessions().catch(() => {});
|
||||||
|
}, 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Listmonk scheduled full sync (every 6h)
|
||||||
|
if (env.LISTMONK_SYNC_ENABLED === 'true') {
|
||||||
|
setInterval(() => {
|
||||||
|
listmonkSyncService.syncAll().catch(() => {});
|
||||||
|
}, 6 * 60 * 60 * 1000);
|
||||||
|
logger.info('Listmonk scheduled full sync enabled (every 6h)');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean old tracking data on startup + daily
|
||||||
trackingService.cleanupOldData(30).catch(() => {});
|
trackingService.cleanupOldData(30).catch(() => {});
|
||||||
|
setInterval(() => trackingService.cleanupOldData(30).catch(() => {}), 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Close stale tracking sessions (no data for 2h) — hourly
|
||||||
trackingService.closeStaleTrackingSessions(120).catch(() => {});
|
trackingService.closeStaleTrackingSessions(120).catch(() => {});
|
||||||
|
setInterval(() => trackingService.closeStaleTrackingSessions(120).catch(() => {}), 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Clean old docs analytics data on startup + daily (90-day retention)
|
||||||
docsAnalyticsService.cleanupOldData(90).catch(() => {});
|
docsAnalyticsService.cleanupOldData(90).catch(() => {});
|
||||||
|
setInterval(() => docsAnalyticsService.cleanupOldData(90).catch(() => {}), 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Volunteer re-engagement scanner — daily
|
||||||
reengagementService.scan().catch(() => {});
|
reengagementService.scan().catch(() => {});
|
||||||
|
setInterval(() => reengagementService.scan().catch(() => {}), 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Social digest email scanner — daily
|
||||||
socialDigestService.scan().catch(() => {});
|
socialDigestService.scan().catch(() => {});
|
||||||
|
setInterval(() => socialDigestService.scan().catch(() => {}), 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
// SSE + Presence: mark all users offline on startup, start heartbeat + stale cleanup
|
// SSE + Presence: mark all users offline on startup, start heartbeat + stale cleanup
|
||||||
presenceService.markAllOffline().catch(() => {});
|
presenceService.markAllOffline().catch(() => {});
|
||||||
@ -407,7 +438,7 @@ async function start() {
|
|||||||
logger.warn('Startup sync of MkDocs overrides failed:', err);
|
logger.warn('Startup sync of MkDocs overrides failed:', err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate MkDocs exports on startup (recurring runs handled by scheduled-jobs queue)
|
// Validate MkDocs exports on startup
|
||||||
pagesService.validateExports()
|
pagesService.validateExports()
|
||||||
.then(({ validated, repaired, errors }) => {
|
.then(({ validated, repaired, errors }) => {
|
||||||
if (repaired > 0 || errors.length > 0) {
|
if (repaired > 0 || errors.length > 0) {
|
||||||
@ -416,6 +447,13 @@ async function start() {
|
|||||||
})
|
})
|
||||||
.catch((err) => logger.warn('Validation failed:', err));
|
.catch((err) => logger.warn('Validation failed:', err));
|
||||||
|
|
||||||
|
// Schedule daily validation
|
||||||
|
setInterval(() => {
|
||||||
|
pagesService.validateExports().catch((err) => {
|
||||||
|
logger.warn('Scheduled validation failed:', err);
|
||||||
|
});
|
||||||
|
}, 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
const server = app.listen(env.PORT, () => {
|
const server = app.listen(env.PORT, () => {
|
||||||
logger.info(`API server running on port ${env.PORT} [${env.NODE_ENV}]`);
|
logger.info(`API server running on port ${env.PORT} [${env.NODE_ENV}]`);
|
||||||
});
|
});
|
||||||
@ -439,8 +477,9 @@ async function start() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean stale collab states on startup (recurring runs handled by scheduled-jobs queue)
|
// Clean stale collab states on startup + daily
|
||||||
docsCollabService.cleanupStaleStates().catch(() => {});
|
docsCollabService.cleanupStaleStates().catch(() => {});
|
||||||
|
setInterval(() => docsCollabService.cleanupStaleStates().catch(() => {}), 24 * 60 * 60 * 1000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('Failed to start server:', err);
|
logger.error('Failed to start server:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@ -461,7 +500,6 @@ for (const signal of ['SIGTERM', 'SIGINT']) {
|
|||||||
await geocodeQueueService.close();
|
await geocodeQueueService.close();
|
||||||
await smsQueueService.close();
|
await smsQueueService.close();
|
||||||
await calendarFeedQueueService.close();
|
await calendarFeedQueueService.close();
|
||||||
await scheduledJobsQueueService.close();
|
|
||||||
await prisma.$disconnect();
|
await prisma.$disconnect();
|
||||||
redis.disconnect();
|
redis.disconnect();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
@ -2,8 +2,6 @@ import { Queue, Worker, type Job } from 'bullmq';
|
|||||||
import { env } from '../config/env';
|
import { env } from '../config/env';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { emailService } from './email.service';
|
import { emailService } from './email.service';
|
||||||
import { prisma } from '../config/database';
|
|
||||||
import { notificationService } from '../modules/social/notification.service';
|
|
||||||
|
|
||||||
// ─── Job Data Types ────────────────────────────────────────────────
|
// ─── Job Data Types ────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -119,26 +117,6 @@ type NotificationJobData =
|
|||||||
|
|
||||||
// ─── Queue Service ─────────────────────────────────────────────────
|
// ─── Queue Service ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Resolve userId from email for in-app notification bridging */
|
|
||||||
async function resolveUserId(email: string): Promise<string | null> {
|
|
||||||
const user = await prisma.user.findUnique({ where: { email }, select: { id: true } });
|
|
||||||
return user?.id ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Fire-and-forget in-app notification creation */
|
|
||||||
function bridgeToInApp(
|
|
||||||
email: string,
|
|
||||||
type: 'shift_signup_confirmed' | 'shift_reminder' | 'shift_cancelled' | 'canvass_session_summary' | 'reengagement',
|
|
||||||
title: string,
|
|
||||||
message: string,
|
|
||||||
metadata?: Record<string, unknown>,
|
|
||||||
) {
|
|
||||||
resolveUserId(email).then((userId) => {
|
|
||||||
if (!userId) return;
|
|
||||||
notificationService.createNotification(userId, type, title, message, metadata);
|
|
||||||
}).catch((err) => logger.warn('Failed to bridge in-app notification', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotificationQueueService {
|
class NotificationQueueService {
|
||||||
private queue: Queue;
|
private queue: Queue;
|
||||||
private worker: Worker | null = null;
|
private worker: Worker | null = null;
|
||||||
@ -177,21 +155,9 @@ class NotificationQueueService {
|
|||||||
break;
|
break;
|
||||||
case 'volunteer-session-summary':
|
case 'volunteer-session-summary':
|
||||||
await emailService.sendVolunteerSessionSummary(data);
|
await emailService.sendVolunteerSessionSummary(data);
|
||||||
bridgeToInApp(
|
|
||||||
data.volunteerEmail, 'canvass_session_summary',
|
|
||||||
'Canvass Session Complete',
|
|
||||||
`You visited ${data.visitCount} addresses in ${data.cutName}`,
|
|
||||||
{ cutName: data.cutName, visitCount: data.visitCount, durationMinutes: data.durationMinutes },
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'volunteer-cancellation':
|
case 'volunteer-cancellation':
|
||||||
await emailService.sendVolunteerCancellationAck(data);
|
await emailService.sendVolunteerCancellationAck(data);
|
||||||
bridgeToInApp(
|
|
||||||
data.volunteerEmail, 'shift_cancelled',
|
|
||||||
'Shift Cancelled',
|
|
||||||
`Your shift "${data.shiftTitle}" on ${data.shiftDate} has been cancelled`,
|
|
||||||
{ shiftTitle: data.shiftTitle, shiftDate: data.shiftDate },
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'volunteer-shift-reminder':
|
case 'volunteer-shift-reminder':
|
||||||
await emailService.sendShiftDetailsEmail({
|
await emailService.sendShiftDetailsEmail({
|
||||||
@ -207,12 +173,6 @@ class NotificationQueueService {
|
|||||||
maxVolunteers: data.maxVolunteers,
|
maxVolunteers: data.maxVolunteers,
|
||||||
shiftStatus: data.shiftStatus,
|
shiftStatus: data.shiftStatus,
|
||||||
});
|
});
|
||||||
bridgeToInApp(
|
|
||||||
data.recipientEmail, 'shift_reminder',
|
|
||||||
'Shift Reminder',
|
|
||||||
`Reminder: "${data.shiftTitle}" on ${data.shiftDate} at ${data.shiftStartTime}`,
|
|
||||||
{ shiftTitle: data.shiftTitle, shiftDate: data.shiftDate, shiftLocation: data.shiftLocation },
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case 'volunteer-shift-thank-you':
|
case 'volunteer-shift-thank-you':
|
||||||
await emailService.sendVolunteerShiftThankYou(data);
|
await emailService.sendVolunteerShiftThankYou(data);
|
||||||
|
|||||||
@ -1,160 +0,0 @@
|
|||||||
import { Queue, Worker, type Job } from 'bullmq';
|
|
||||||
import { env } from '../config/env';
|
|
||||||
import { logger } from '../utils/logger';
|
|
||||||
|
|
||||||
const QUEUE_NAME = 'scheduled-jobs';
|
|
||||||
|
|
||||||
type ScheduledJobType =
|
|
||||||
| 'reengagement-scan'
|
|
||||||
| 'social-digest-scan'
|
|
||||||
| 'close-abandoned-canvass-sessions'
|
|
||||||
| 'close-stale-tracking-sessions'
|
|
||||||
| 'cleanup-tracking-data'
|
|
||||||
| 'cleanup-docs-analytics'
|
|
||||||
| 'cleanup-verification-tokens'
|
|
||||||
| 'listmonk-full-sync'
|
|
||||||
| 'validate-mkdocs-exports'
|
|
||||||
| 'cleanup-docs-collab-states';
|
|
||||||
|
|
||||||
interface ScheduledJobData {
|
|
||||||
type: ScheduledJobType;
|
|
||||||
}
|
|
||||||
|
|
||||||
const HOUR = 60 * 60 * 1000;
|
|
||||||
|
|
||||||
const JOB_DEFINITIONS: Array<{ type: ScheduledJobType; every: number; conditional?: boolean }> = [
|
|
||||||
{ type: 'reengagement-scan', every: 24 * HOUR },
|
|
||||||
{ type: 'social-digest-scan', every: 24 * HOUR },
|
|
||||||
{ type: 'close-abandoned-canvass-sessions', every: HOUR },
|
|
||||||
{ type: 'close-stale-tracking-sessions', every: HOUR },
|
|
||||||
{ type: 'cleanup-tracking-data', every: 24 * HOUR },
|
|
||||||
{ type: 'cleanup-docs-analytics', every: 24 * HOUR },
|
|
||||||
{ type: 'cleanup-verification-tokens', every: HOUR },
|
|
||||||
{ type: 'listmonk-full-sync', every: 6 * HOUR, conditional: true },
|
|
||||||
{ type: 'validate-mkdocs-exports', every: 24 * HOUR },
|
|
||||||
{ type: 'cleanup-docs-collab-states', every: 24 * HOUR },
|
|
||||||
];
|
|
||||||
|
|
||||||
async function executeJob(type: ScheduledJobType): Promise<void> {
|
|
||||||
switch (type) {
|
|
||||||
case 'reengagement-scan': {
|
|
||||||
const { reengagementService } = await import('./reengagement.service');
|
|
||||||
await reengagementService.scan();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'social-digest-scan': {
|
|
||||||
const { socialDigestService } = await import('./social-digest.service');
|
|
||||||
await socialDigestService.scan();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'close-abandoned-canvass-sessions': {
|
|
||||||
const { canvassService } = await import('../modules/map/canvass/canvass.service');
|
|
||||||
await canvassService.closeAbandonedSessions();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'close-stale-tracking-sessions': {
|
|
||||||
const { trackingService } = await import('../modules/map/tracking/tracking.service');
|
|
||||||
await trackingService.closeStaleTrackingSessions(120);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cleanup-tracking-data': {
|
|
||||||
const { trackingService } = await import('../modules/map/tracking/tracking.service');
|
|
||||||
await trackingService.cleanupOldData(30);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cleanup-docs-analytics': {
|
|
||||||
const { docsAnalyticsService } = await import('../modules/docs-analytics/docs-analytics.service');
|
|
||||||
await docsAnalyticsService.cleanupOldData(90);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cleanup-verification-tokens': {
|
|
||||||
const { verificationTokenService } = await import('./verification-token.service');
|
|
||||||
const { passwordResetTokenService } = await import('./password-reset-token.service');
|
|
||||||
await verificationTokenService.cleanupExpiredTokens();
|
|
||||||
await passwordResetTokenService.cleanupExpiredTokens();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'listmonk-full-sync': {
|
|
||||||
const { listmonkSyncService } = await import('./listmonk-sync.service');
|
|
||||||
await listmonkSyncService.syncAll();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'validate-mkdocs-exports': {
|
|
||||||
const { pagesService } = await import('../modules/pages/pages.service');
|
|
||||||
await pagesService.validateExports();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'cleanup-docs-collab-states': {
|
|
||||||
const { docsCollabService } = await import('../modules/docs/docs-collab.service');
|
|
||||||
await docsCollabService.cleanupStaleStates();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ScheduledJobsQueueService {
|
|
||||||
private queue: Queue;
|
|
||||||
private worker: Worker | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.queue = new Queue(QUEUE_NAME, {
|
|
||||||
connection: { url: env.REDIS_URL },
|
|
||||||
defaultJobOptions: {
|
|
||||||
removeOnComplete: { age: 60 * 60, count: 200 },
|
|
||||||
removeOnFail: { age: 24 * 60 * 60 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
startWorker() {
|
|
||||||
// Register repeatable jobs
|
|
||||||
for (const def of JOB_DEFINITIONS) {
|
|
||||||
// Skip conditional jobs when their feature is disabled
|
|
||||||
if (def.type === 'listmonk-full-sync' && env.LISTMONK_SYNC_ENABLED !== 'true') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queue.add(
|
|
||||||
def.type,
|
|
||||||
{ type: def.type } satisfies ScheduledJobData,
|
|
||||||
{
|
|
||||||
repeat: { every: def.every },
|
|
||||||
jobId: `scheduled-${def.type}`,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.worker = new Worker(
|
|
||||||
QUEUE_NAME,
|
|
||||||
async (job: Job<ScheduledJobData>) => {
|
|
||||||
const { type } = job.data;
|
|
||||||
logger.debug(`Scheduled job starting: ${type}`);
|
|
||||||
await executeJob(type);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
connection: { url: env.REDIS_URL },
|
|
||||||
concurrency: 2,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.worker.on('completed', (job) => {
|
|
||||||
logger.debug(`Scheduled job ${job.name} completed`);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.worker.on('failed', (job, err) => {
|
|
||||||
logger.error(`Scheduled job ${job?.name} failed: ${err.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.info('Scheduled jobs queue worker started (10 job types)');
|
|
||||||
}
|
|
||||||
|
|
||||||
async close() {
|
|
||||||
if (this.worker) {
|
|
||||||
await this.worker.close();
|
|
||||||
}
|
|
||||||
await this.queue.close();
|
|
||||||
logger.info('Scheduled jobs queue closed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const scheduledJobsQueueService = new ScheduledJobsQueueService();
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
import { ContactActivityType } from '@prisma/client';
|
|
||||||
import { prisma } from '../config/database';
|
|
||||||
import { logger } from './logger';
|
|
||||||
|
|
||||||
interface RecordActivityParams {
|
|
||||||
userId?: string;
|
|
||||||
email?: string;
|
|
||||||
contactId?: string;
|
|
||||||
activityType: ContactActivityType;
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
metadata?: Record<string, unknown>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fire-and-forget CRM activity recorder.
|
|
||||||
* Resolves a Contact by userId or email, then writes a ContactActivity row.
|
|
||||||
* Skips silently if no matching Contact is found (anonymous users).
|
|
||||||
*/
|
|
||||||
export async function recordCrmActivity(params: RecordActivityParams): Promise<void> {
|
|
||||||
try {
|
|
||||||
let contactId = params.contactId;
|
|
||||||
|
|
||||||
if (!contactId) {
|
|
||||||
const conditions: Record<string, unknown>[] = [];
|
|
||||||
if (params.userId) conditions.push({ userId: params.userId });
|
|
||||||
if (params.email) conditions.push({ email: params.email });
|
|
||||||
|
|
||||||
if (conditions.length === 0) return;
|
|
||||||
|
|
||||||
const contact = await prisma.contact.findFirst({
|
|
||||||
where: {
|
|
||||||
mergedIntoId: null,
|
|
||||||
OR: conditions,
|
|
||||||
},
|
|
||||||
select: { id: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!contact) return;
|
|
||||||
contactId = contact.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.contactActivity.create({
|
|
||||||
data: {
|
|
||||||
contactId,
|
|
||||||
type: params.activityType,
|
|
||||||
title: params.title,
|
|
||||||
description: params.description,
|
|
||||||
metadata: params.metadata as unknown as import('@prisma/client').Prisma.InputJsonValue,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('Failed to record CRM activity', { error: err instanceof Error ? err.message : String(err), params: { activityType: params.activityType } });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,8 +15,8 @@ services:
|
|||||||
container_name: changemaker-v2-api
|
container_name: changemaker-v2-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${API_PORT:-4000}:4000"
|
- "${API_PORT:-4000}:4000"
|
||||||
- "127.0.0.1:${LISTMONK_PROXY_PORT:-9002}:9002"
|
- "${LISTMONK_PROXY_PORT:-9002}:9002"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"]
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4000/api/health"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@ -69,7 +69,7 @@ services:
|
|||||||
- VAULTWARDEN_EMBED_PORT=${VAULTWARDEN_EMBED_PORT:-8890}
|
- VAULTWARDEN_EMBED_PORT=${VAULTWARDEN_EMBED_PORT:-8890}
|
||||||
- ROCKETCHAT_URL=${ROCKETCHAT_URL:-http://rocketchat-changemaker:3000}
|
- ROCKETCHAT_URL=${ROCKETCHAT_URL:-http://rocketchat-changemaker:3000}
|
||||||
- ROCKETCHAT_ADMIN_USER=${ROCKETCHAT_ADMIN_USER:-rcadmin}
|
- ROCKETCHAT_ADMIN_USER=${ROCKETCHAT_ADMIN_USER:-rcadmin}
|
||||||
- ROCKETCHAT_ADMIN_PASSWORD=${ROCKETCHAT_ADMIN_PASSWORD:?ROCKETCHAT_ADMIN_PASSWORD must be set in .env}
|
- ROCKETCHAT_ADMIN_PASSWORD=${ROCKETCHAT_ADMIN_PASSWORD:-changeme}
|
||||||
- ROCKETCHAT_EMBED_PORT=${ROCKETCHAT_EMBED_PORT:-8891}
|
- ROCKETCHAT_EMBED_PORT=${ROCKETCHAT_EMBED_PORT:-8891}
|
||||||
- ENABLE_CHAT=${ENABLE_CHAT:-false}
|
- ENABLE_CHAT=${ENABLE_CHAT:-false}
|
||||||
- GANCIO_URL=${GANCIO_URL:-http://gancio-changemaker:13120}
|
- GANCIO_URL=${GANCIO_URL:-http://gancio-changemaker:13120}
|
||||||
@ -129,7 +129,7 @@ services:
|
|||||||
container_name: changemaker-media-api
|
container_name: changemaker-media-api
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${MEDIA_API_PORT:-4100}:4100"
|
- "${MEDIA_API_PORT:-4100}:4100"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"]
|
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:4100/health"]
|
||||||
interval: 15s
|
interval: 15s
|
||||||
@ -148,7 +148,6 @@ services:
|
|||||||
- MEDIA_ROOT=/media/local
|
- MEDIA_ROOT=/media/local
|
||||||
- MEDIA_UPLOADS=/media/uploads
|
- MEDIA_UPLOADS=/media/uploads
|
||||||
- MAX_UPLOAD_SIZE_GB=${MAX_UPLOAD_SIZE_GB:-10}
|
- MAX_UPLOAD_SIZE_GB=${MAX_UPLOAD_SIZE_GB:-10}
|
||||||
- INITIAL_ADMIN_PASSWORD=${INITIAL_ADMIN_PASSWORD}
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./api:/app
|
- ./api:/app
|
||||||
- /app/node_modules
|
- /app/node_modules
|
||||||
@ -179,7 +178,7 @@ services:
|
|||||||
container_name: changemaker-v2-admin
|
container_name: changemaker-v2-admin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${ADMIN_PORT:-3000}:3000"
|
- "${ADMIN_PORT:-3000}:3000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"]
|
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -267,11 +266,11 @@ services:
|
|||||||
|
|
||||||
# NocoDB v2 — pointed at v2 PostgreSQL as read-only data browser
|
# NocoDB v2 — pointed at v2 PostgreSQL as read-only data browser
|
||||||
nocodb-v2:
|
nocodb-v2:
|
||||||
image: nocodb/nocodb:0.301.3
|
image: nocodb/nocodb:latest
|
||||||
container_name: changemaker-v2-nocodb
|
container_name: changemaker-v2-nocodb
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${NOCODB_V2_PORT:-8091}:8080"
|
- "${NOCODB_V2_PORT:-8091}:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/v1/health"]
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/api/v1/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -353,11 +352,11 @@ services:
|
|||||||
|
|
||||||
# Listmonk — Email marketing (kept as Docker image, controlled via REST API)
|
# Listmonk — Email marketing (kept as Docker image, controlled via REST API)
|
||||||
listmonk-app:
|
listmonk-app:
|
||||||
image: listmonk/listmonk:v6.0.0
|
image: listmonk/listmonk:latest
|
||||||
container_name: listmonk-app
|
container_name: listmonk-app
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${LISTMONK_PORT:-9001}:9000"
|
- "${LISTMONK_PORT:-9001}:9000"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"]
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -488,16 +487,9 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./configs/code-server/.config:/home/coder/.config
|
- ./configs/code-server/.config:/home/coder/.config
|
||||||
- ./configs/code-server/.local:/home/coder/.local
|
- ./configs/code-server/.local:/home/coder/.local
|
||||||
- ./api:/home/coder/project/api
|
- .:/home/coder/project
|
||||||
- ./admin:/home/coder/project/admin
|
|
||||||
- ./nginx:/home/coder/project/nginx
|
|
||||||
- ./configs:/home/coder/project/configs
|
|
||||||
- ./scripts:/home/coder/project/scripts
|
|
||||||
- ./mkdocs:/home/coder/project/mkdocs
|
|
||||||
- ./docker-compose.yml:/home/coder/project/docker-compose.yml
|
|
||||||
# NOTE: .env intentionally excluded — secrets must not be accessible via Code Server
|
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${CODE_SERVER_PORT:-8888}:8080"
|
- "${CODE_SERVER_PORT:-8888}:8080"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- changemaker-lite
|
- changemaker-lite
|
||||||
@ -513,7 +505,7 @@ services:
|
|||||||
- ./scripts/mkdocs-entrypoint.sh:/scripts/mkdocs-entrypoint.sh:ro
|
- ./scripts/mkdocs-entrypoint.sh:/scripts/mkdocs-entrypoint.sh:ro
|
||||||
user: "${USER_ID:-1000}:${GROUP_ID:-1000}"
|
user: "${USER_ID:-1000}:${GROUP_ID:-1000}"
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${MKDOCS_PORT:-4003}:8000"
|
- "${MKDOCS_PORT:-4003}:8000"
|
||||||
environment:
|
environment:
|
||||||
- SITE_URL=${BASE_DOMAIN:-https://cmlite.org}
|
- SITE_URL=${BASE_DOMAIN:-https://cmlite.org}
|
||||||
- ADMIN_PORT=${ADMIN_PORT:-3000}
|
- ADMIN_PORT=${ADMIN_PORT:-3000}
|
||||||
@ -532,7 +524,7 @@ services:
|
|||||||
|
|
||||||
# MkDocs built site — Nginx static server
|
# MkDocs built site — Nginx static server
|
||||||
mkdocs-site-server:
|
mkdocs-site-server:
|
||||||
image: lscr.io/linuxserver/nginx:1.28.2
|
image: lscr.io/linuxserver/nginx:latest
|
||||||
container_name: mkdocs-site-server-changemaker
|
container_name: mkdocs-site-server-changemaker
|
||||||
environment:
|
environment:
|
||||||
- PUID=${USER_ID:-1000}
|
- PUID=${USER_ID:-1000}
|
||||||
@ -542,7 +534,7 @@ services:
|
|||||||
- ./mkdocs/site:/config/www
|
- ./mkdocs/site:/config/www
|
||||||
- ./configs/mkdocs-site/default.conf:/config/nginx/site-confs/default.conf
|
- ./configs/mkdocs-site/default.conf:/config/nginx/site-confs/default.conf
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${MKDOCS_SITE_SERVER_PORT:-4004}:80"
|
- "${MKDOCS_SITE_SERVER_PORT:-4004}:80"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- changemaker-lite
|
- changemaker-lite
|
||||||
@ -553,7 +545,7 @@ services:
|
|||||||
container_name: n8n-changemaker
|
container_name: n8n-changemaker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${N8N_PORT:-5678}:5678"
|
- "${N8N_PORT:-5678}:5678"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"]
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:5678/healthz"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -579,10 +571,10 @@ services:
|
|||||||
|
|
||||||
# Homepage dashboard
|
# Homepage dashboard
|
||||||
homepage:
|
homepage:
|
||||||
image: ghcr.io/gethomepage/homepage:v0.7.2
|
image: ghcr.io/gethomepage/homepage:latest
|
||||||
container_name: homepage-changemaker
|
container_name: homepage-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${HOMEPAGE_PORT:-3010}:3000"
|
- "${HOMEPAGE_PORT:-3010}:3000"
|
||||||
volumes:
|
volumes:
|
||||||
- ./configs/homepage:/app/config
|
- ./configs/homepage:/app/config
|
||||||
- ./assets/icons:/app/public/icons
|
- ./assets/icons:/app/public/icons
|
||||||
@ -632,8 +624,8 @@ services:
|
|||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${GITEA_WEB_PORT:-3030}:3000"
|
- "${GITEA_WEB_PORT:-3030}:3000"
|
||||||
- "127.0.0.1:${GITEA_SSH_PORT:-2222}:22"
|
- "${GITEA_SSH_PORT:-2222}:22"
|
||||||
depends_on:
|
depends_on:
|
||||||
- gitea-db
|
- gitea-db
|
||||||
networks:
|
networks:
|
||||||
@ -660,21 +652,21 @@ services:
|
|||||||
|
|
||||||
# Mini QR — QR code generator
|
# Mini QR — QR code generator
|
||||||
mini-qr:
|
mini-qr:
|
||||||
image: ghcr.io/lyqht/mini-qr:v0.26.0
|
image: ghcr.io/lyqht/mini-qr:latest
|
||||||
container_name: mini-qr
|
container_name: mini-qr
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${MINI_QR_PORT:-8089}:8080"
|
- "${MINI_QR_PORT:-8089}:8080"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- changemaker-lite
|
- changemaker-lite
|
||||||
|
|
||||||
# Excalidraw — Collaborative whiteboard
|
# Excalidraw — Collaborative whiteboard
|
||||||
excalidraw:
|
excalidraw:
|
||||||
image: kiliandeca/excalidraw:sha-e42a510
|
image: kiliandeca/excalidraw:latest
|
||||||
container_name: excalidraw-changemaker
|
container_name: excalidraw-changemaker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${EXCALIDRAW_PORT:-8090}:80"
|
- "${EXCALIDRAW_PORT:-8090}:80"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"]
|
test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -688,11 +680,11 @@ services:
|
|||||||
|
|
||||||
# Vaultwarden — Password manager (Bitwarden-compatible)
|
# Vaultwarden — Password manager (Bitwarden-compatible)
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
image: vaultwarden/server:1.35.4
|
image: vaultwarden/server:latest
|
||||||
container_name: vaultwarden-changemaker
|
container_name: vaultwarden-changemaker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${VAULTWARDEN_PORT:-8445}:80"
|
- "${VAULTWARDEN_PORT:-8445}:80"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:80/alive"]
|
test: ["CMD", "curl", "-sf", "http://localhost:80/alive"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -722,7 +714,7 @@ services:
|
|||||||
# Uses the admin panel API to send an invitation email (lands in MailHog or real SMTP).
|
# Uses the admin panel API to send an invitation email (lands in MailHog or real SMTP).
|
||||||
# Safe to re-run (Vaultwarden ignores duplicate invites for existing users). Exits 0 on success.
|
# Safe to re-run (Vaultwarden ignores duplicate invites for existing users). Exits 0 on success.
|
||||||
vaultwarden-init:
|
vaultwarden-init:
|
||||||
image: alpine/curl:8.11.1
|
image: alpine/curl:latest
|
||||||
container_name: vaultwarden-init
|
container_name: vaultwarden-init
|
||||||
depends_on:
|
depends_on:
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
@ -858,14 +850,14 @@ services:
|
|||||||
|
|
||||||
# Gancio — Event management platform (uses shared PostgreSQL)
|
# Gancio — Event management platform (uses shared PostgreSQL)
|
||||||
gancio:
|
gancio:
|
||||||
image: cisti/gancio:1.28.2
|
image: cisti/gancio:latest
|
||||||
container_name: gancio-changemaker
|
container_name: gancio-changemaker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
v2-postgres:
|
v2-postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${GANCIO_PORT:-8092}:13120"
|
- "${GANCIO_PORT:-8092}:13120"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"]
|
test: ["CMD", "node", "-e", "require('http').get('http://localhost:13120/', r => process.exit(r.statusCode < 400 ? 0 : 1)).on('error', () => process.exit(1))"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@ -1031,7 +1023,7 @@ services:
|
|||||||
jitsi-prosody:
|
jitsi-prosody:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${JVB_PORT:-10000}:10000/udp"
|
- "${JVB_PORT:-10000}:10000/udp"
|
||||||
environment:
|
environment:
|
||||||
- XMPP_DOMAIN=meet.jitsi
|
- XMPP_DOMAIN=meet.jitsi
|
||||||
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
|
- XMPP_AUTH_DOMAIN=auth.meet.jitsi
|
||||||
@ -1049,10 +1041,10 @@ services:
|
|||||||
|
|
||||||
# MailHog — Email testing (dev)
|
# MailHog — Email testing (dev)
|
||||||
mailhog:
|
mailhog:
|
||||||
image: mailhog/mailhog:v1.0.1
|
image: mailhog/mailhog:latest
|
||||||
container_name: mailhog-changemaker
|
container_name: mailhog-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${MAILHOG_WEB_PORT:-8025}:8025"
|
- "${MAILHOG_WEB_PORT:-8025}:8025"
|
||||||
# SMTP port 1025 is only exposed on the Docker network (containers connect via mailhog-changemaker:1025)
|
# SMTP port 1025 is only exposed on the Docker network (containers connect via mailhog-changemaker:1025)
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
@ -1083,7 +1075,7 @@ services:
|
|||||||
|
|
||||||
# Docker socket proxy — read-only access for container status monitoring
|
# Docker socket proxy — read-only access for container status monitoring
|
||||||
docker-socket-proxy:
|
docker-socket-proxy:
|
||||||
image: tecnativa/docker-socket-proxy:0.4.2
|
image: tecnativa/docker-socket-proxy:latest
|
||||||
container_name: docker-socket-proxy
|
container_name: docker-socket-proxy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@ -1104,14 +1096,14 @@ services:
|
|||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|
||||||
prometheus:
|
prometheus:
|
||||||
image: prom/prometheus:v3.10.0
|
image: prom/prometheus:latest
|
||||||
container_name: prometheus-changemaker
|
container_name: prometheus-changemaker
|
||||||
command:
|
command:
|
||||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||||
- '--storage.tsdb.path=/prometheus'
|
- '--storage.tsdb.path=/prometheus'
|
||||||
- '--storage.tsdb.retention.time=30d'
|
- '--storage.tsdb.retention.time=30d'
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${PROMETHEUS_PORT:-9090}:9090"
|
- "${PROMETHEUS_PORT:-9090}:9090"
|
||||||
volumes:
|
volumes:
|
||||||
- ./configs/prometheus:/etc/prometheus
|
- ./configs/prometheus:/etc/prometheus
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@ -1122,10 +1114,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:12.3.0
|
image: grafana/grafana:latest
|
||||||
container_name: grafana-changemaker
|
container_name: grafana-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${GRAFANA_PORT:-3001}:3000"
|
- "${GRAFANA_PORT:-3001}:3000"
|
||||||
environment:
|
environment:
|
||||||
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD must be set in .env}
|
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD must be set in .env}
|
||||||
- GF_USERS_ALLOW_SIGN_UP=false
|
- GF_USERS_ALLOW_SIGN_UP=false
|
||||||
@ -1145,10 +1137,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
cadvisor:
|
cadvisor:
|
||||||
image: gcr.io/cadvisor/cadvisor:v0.55.1
|
image: gcr.io/cadvisor/cadvisor:latest
|
||||||
container_name: cadvisor-changemaker
|
container_name: cadvisor-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${CADVISOR_PORT:-8080}:8080"
|
- "${CADVISOR_PORT:-8080}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- /:/rootfs:ro
|
- /:/rootfs:ro
|
||||||
- /var/run:/var/run:ro
|
- /var/run:/var/run:ro
|
||||||
@ -1156,7 +1148,6 @@ services:
|
|||||||
- /var/lib/docker/:/var/lib/docker:ro
|
- /var/lib/docker/:/var/lib/docker:ro
|
||||||
- /dev/disk/:/dev/disk:ro
|
- /dev/disk/:/dev/disk:ro
|
||||||
privileged: true
|
privileged: true
|
||||||
read_only: true
|
|
||||||
devices:
|
devices:
|
||||||
- /dev/kmsg
|
- /dev/kmsg
|
||||||
restart: always
|
restart: always
|
||||||
@ -1166,10 +1157,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
node-exporter:
|
node-exporter:
|
||||||
image: prom/node-exporter:v1.10.2
|
image: prom/node-exporter:latest
|
||||||
container_name: node-exporter-changemaker
|
container_name: node-exporter-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${NODE_EXPORTER_PORT:-9100}:9100"
|
- "${NODE_EXPORTER_PORT:-9100}:9100"
|
||||||
command:
|
command:
|
||||||
- '--path.rootfs=/host'
|
- '--path.rootfs=/host'
|
||||||
- '--path.procfs=/host/proc'
|
- '--path.procfs=/host/proc'
|
||||||
@ -1186,10 +1177,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
redis-exporter:
|
redis-exporter:
|
||||||
image: oliver006/redis_exporter:v1.81.0
|
image: oliver006/redis_exporter:latest
|
||||||
container_name: redis-exporter-changemaker
|
container_name: redis-exporter-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${REDIS_EXPORTER_PORT:-9121}:9121"
|
- "${REDIS_EXPORTER_PORT:-9121}:9121"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_ADDR=redis://redis-changemaker:6379
|
- REDIS_ADDR=redis://redis-changemaker:6379
|
||||||
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
- REDIS_PASSWORD=${REDIS_PASSWORD}
|
||||||
@ -1202,10 +1193,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: prom/alertmanager:v0.31.1
|
image: prom/alertmanager:latest
|
||||||
container_name: alertmanager-changemaker
|
container_name: alertmanager-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${ALERTMANAGER_PORT:-9093}:9093"
|
- "${ALERTMANAGER_PORT:-9093}:9093"
|
||||||
volumes:
|
volumes:
|
||||||
- ./configs/alertmanager:/etc/alertmanager
|
- ./configs/alertmanager:/etc/alertmanager
|
||||||
- alertmanager-data:/alertmanager
|
- alertmanager-data:/alertmanager
|
||||||
@ -1219,10 +1210,10 @@ services:
|
|||||||
- monitoring
|
- monitoring
|
||||||
|
|
||||||
gotify:
|
gotify:
|
||||||
image: gotify/server:v2.9.0
|
image: gotify/server:latest
|
||||||
container_name: gotify-changemaker
|
container_name: gotify-changemaker
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${GOTIFY_PORT:-8889}:80"
|
- "${GOTIFY_PORT:-8889}:80"
|
||||||
environment:
|
environment:
|
||||||
- GOTIFY_DEFAULTUSER_NAME=${GOTIFY_ADMIN_USER:-admin}
|
- GOTIFY_DEFAULTUSER_NAME=${GOTIFY_ADMIN_USER:-admin}
|
||||||
- GOTIFY_DEFAULTUSER_PASS=${GOTIFY_ADMIN_PASSWORD:?GOTIFY_ADMIN_PASSWORD must be set in .env}
|
- GOTIFY_DEFAULTUSER_PASS=${GOTIFY_ADMIN_PASSWORD:?GOTIFY_ADMIN_PASSWORD must be set in .env}
|
||||||
|
|||||||
@ -7,10 +7,10 @@
|
|||||||
"stars_count": 0,
|
"stars_count": 0,
|
||||||
"forks_count": 0,
|
"forks_count": 0,
|
||||||
"open_issues_count": 23,
|
"open_issues_count": 23,
|
||||||
"updated_at": "2026-03-09T12:23:17-06:00",
|
"updated_at": "2026-03-08T18:11:30-06:00",
|
||||||
"created_at": "2025-05-28T14:54:59-06:00",
|
"created_at": "2025-05-28T14:54:59-06:00",
|
||||||
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
|
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
|
||||||
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
|
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T12:23:17-06:00"
|
"last_build_update": "2026-03-08T18:11:30-06:00"
|
||||||
}
|
}
|
||||||
@ -4,10 +4,10 @@
|
|||||||
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
|
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
|
||||||
"html_url": "https://github.com/anthropics/claude-code",
|
"html_url": "https://github.com/anthropics/claude-code",
|
||||||
"language": "Shell",
|
"language": "Shell",
|
||||||
"stars_count": 75798,
|
"stars_count": 75344,
|
||||||
"forks_count": 6114,
|
"forks_count": 6074,
|
||||||
"open_issues_count": 5868,
|
"open_issues_count": 5793,
|
||||||
"updated_at": "2026-03-09T21:59:41Z",
|
"updated_at": "2026-03-09T00:15:16Z",
|
||||||
"created_at": "2025-02-22T17:41:21Z",
|
"created_at": "2025-02-22T17:41:21Z",
|
||||||
"clone_url": "https://github.com/anthropics/claude-code.git",
|
"clone_url": "https://github.com/anthropics/claude-code.git",
|
||||||
"ssh_url": "git@github.com:anthropics/claude-code.git",
|
"ssh_url": "git@github.com:anthropics/claude-code.git",
|
||||||
|
|||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "VS Code in the browser",
|
"description": "VS Code in the browser",
|
||||||
"html_url": "https://github.com/coder/code-server",
|
"html_url": "https://github.com/coder/code-server",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 76554,
|
"stars_count": 76540,
|
||||||
"forks_count": 6541,
|
"forks_count": 6539,
|
||||||
"open_issues_count": 169,
|
"open_issues_count": 169,
|
||||||
"updated_at": "2026-03-09T20:21:15Z",
|
"updated_at": "2026-03-08T21:32:19Z",
|
||||||
"created_at": "2019-02-27T16:50:41Z",
|
"created_at": "2019-02-27T16:50:41Z",
|
||||||
"clone_url": "https://github.com/coder/code-server.git",
|
"clone_url": "https://github.com/coder/code-server.git",
|
||||||
"ssh_url": "git@github.com:coder/code-server.git",
|
"ssh_url": "git@github.com:coder/code-server.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T19:30:51Z"
|
"last_build_update": "2026-03-06T12:59:10Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
|
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
|
||||||
"html_url": "https://github.com/gethomepage/homepage",
|
"html_url": "https://github.com/gethomepage/homepage",
|
||||||
"language": "JavaScript",
|
"language": "JavaScript",
|
||||||
"stars_count": 28818,
|
"stars_count": 28793,
|
||||||
"forks_count": 1812,
|
"forks_count": 1811,
|
||||||
"open_issues_count": 1,
|
"open_issues_count": 1,
|
||||||
"updated_at": "2026-03-09T21:38:58Z",
|
"updated_at": "2026-03-08T23:44:14Z",
|
||||||
"created_at": "2022-08-24T07:29:42Z",
|
"created_at": "2022-08-24T07:29:42Z",
|
||||||
"clone_url": "https://github.com/gethomepage/homepage.git",
|
"clone_url": "https://github.com/gethomepage/homepage.git",
|
||||||
"ssh_url": "git@github.com:gethomepage/homepage.git",
|
"ssh_url": "git@github.com:gethomepage/homepage.git",
|
||||||
"default_branch": "dev",
|
"default_branch": "dev",
|
||||||
"last_build_update": "2026-03-09T17:02:06Z"
|
"last_build_update": "2026-03-08T12:16:51Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
||||||
"html_url": "https://github.com/go-gitea/gitea",
|
"html_url": "https://github.com/go-gitea/gitea",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 54194,
|
"stars_count": 54180,
|
||||||
"forks_count": 6448,
|
"forks_count": 6438,
|
||||||
"open_issues_count": 2850,
|
"open_issues_count": 2846,
|
||||||
"updated_at": "2026-03-09T21:55:30Z",
|
"updated_at": "2026-03-08T23:25:42Z",
|
||||||
"created_at": "2016-11-01T02:13:26Z",
|
"created_at": "2016-11-01T02:13:26Z",
|
||||||
"clone_url": "https://github.com/go-gitea/gitea.git",
|
"clone_url": "https://github.com/go-gitea/gitea.git",
|
||||||
"ssh_url": "git@github.com:go-gitea/gitea.git",
|
"ssh_url": "git@github.com:go-gitea/gitea.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T10:12:24Z"
|
"last_build_update": "2026-03-08T20:49:59Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
|
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
|
||||||
"html_url": "https://github.com/knadh/listmonk",
|
"html_url": "https://github.com/knadh/listmonk",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 19236,
|
"stars_count": 19221,
|
||||||
"forks_count": 1945,
|
"forks_count": 1945,
|
||||||
"open_issues_count": 99,
|
"open_issues_count": 99,
|
||||||
"updated_at": "2026-03-09T21:14:22Z",
|
"updated_at": "2026-03-08T23:08:55Z",
|
||||||
"created_at": "2019-06-26T05:08:39Z",
|
"created_at": "2019-06-26T05:08:39Z",
|
||||||
"clone_url": "https://github.com/knadh/listmonk.git",
|
"clone_url": "https://github.com/knadh/listmonk.git",
|
||||||
"ssh_url": "git@github.com:knadh/listmonk.git",
|
"ssh_url": "git@github.com:knadh/listmonk.git",
|
||||||
"default_branch": "master",
|
"default_branch": "master",
|
||||||
"last_build_update": "2026-03-09T03:05:44Z"
|
"last_build_update": "2026-03-08T14:04:45Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
|
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
|
||||||
"html_url": "https://github.com/lyqht/mini-qr",
|
"html_url": "https://github.com/lyqht/mini-qr",
|
||||||
"language": "Vue",
|
"language": "Vue",
|
||||||
"stars_count": 1897,
|
"stars_count": 1898,
|
||||||
"forks_count": 241,
|
"forks_count": 240,
|
||||||
"open_issues_count": 22,
|
"open_issues_count": 21,
|
||||||
"updated_at": "2026-03-09T15:15:51Z",
|
"updated_at": "2026-03-08T15:02:09Z",
|
||||||
"created_at": "2023-04-21T14:20:14Z",
|
"created_at": "2023-04-21T14:20:14Z",
|
||||||
"clone_url": "https://github.com/lyqht/mini-qr.git",
|
"clone_url": "https://github.com/lyqht/mini-qr.git",
|
||||||
"ssh_url": "git@github.com:lyqht/mini-qr.git",
|
"ssh_url": "git@github.com:lyqht/mini-qr.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T07:31:47Z"
|
"last_build_update": "2026-03-05T13:18:42Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
|
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
|
||||||
"html_url": "https://github.com/n8n-io/n8n",
|
"html_url": "https://github.com/n8n-io/n8n",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 178343,
|
"stars_count": 178152,
|
||||||
"forks_count": 55592,
|
"forks_count": 55558,
|
||||||
"open_issues_count": 1414,
|
"open_issues_count": 1405,
|
||||||
"updated_at": "2026-03-09T21:52:13Z",
|
"updated_at": "2026-03-09T00:14:18Z",
|
||||||
"created_at": "2019-06-22T09:24:21Z",
|
"created_at": "2019-06-22T09:24:21Z",
|
||||||
"clone_url": "https://github.com/n8n-io/n8n.git",
|
"clone_url": "https://github.com/n8n-io/n8n.git",
|
||||||
"ssh_url": "git@github.com:n8n-io/n8n.git",
|
"ssh_url": "git@github.com:n8n-io/n8n.git",
|
||||||
"default_branch": "master",
|
"default_branch": "master",
|
||||||
"last_build_update": "2026-03-09T21:33:49Z"
|
"last_build_update": "2026-03-09T00:10:51Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
|
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
|
||||||
"html_url": "https://github.com/nocodb/nocodb",
|
"html_url": "https://github.com/nocodb/nocodb",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 62431,
|
"stars_count": 62384,
|
||||||
"forks_count": 4659,
|
"forks_count": 4655,
|
||||||
"open_issues_count": 629,
|
"open_issues_count": 627,
|
||||||
"updated_at": "2026-03-09T19:42:51Z",
|
"updated_at": "2026-03-08T23:36:57Z",
|
||||||
"created_at": "2017-10-29T18:51:48Z",
|
"created_at": "2017-10-29T18:51:48Z",
|
||||||
"clone_url": "https://github.com/nocodb/nocodb.git",
|
"clone_url": "https://github.com/nocodb/nocodb.git",
|
||||||
"ssh_url": "git@github.com:nocodb/nocodb.git",
|
"ssh_url": "git@github.com:nocodb/nocodb.git",
|
||||||
"default_branch": "develop",
|
"default_branch": "develop",
|
||||||
"last_build_update": "2026-03-09T12:25:14Z"
|
"last_build_update": "2026-03-07T10:48:26Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
|
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
|
||||||
"html_url": "https://github.com/ollama/ollama",
|
"html_url": "https://github.com/ollama/ollama",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 164642,
|
"stars_count": 164479,
|
||||||
"forks_count": 14860,
|
"forks_count": 14834,
|
||||||
"open_issues_count": 2609,
|
"open_issues_count": 2613,
|
||||||
"updated_at": "2026-03-09T21:37:49Z",
|
"updated_at": "2026-03-09T00:13:40Z",
|
||||||
"created_at": "2023-06-26T19:39:32Z",
|
"created_at": "2023-06-26T19:39:32Z",
|
||||||
"clone_url": "https://github.com/ollama/ollama.git",
|
"clone_url": "https://github.com/ollama/ollama.git",
|
||||||
"ssh_url": "git@github.com:ollama/ollama.git",
|
"ssh_url": "git@github.com:ollama/ollama.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T21:19:34Z"
|
"last_build_update": "2026-03-08T06:32:28Z"
|
||||||
}
|
}
|
||||||
@ -4,10 +4,10 @@
|
|||||||
"description": "Documentation that simply works",
|
"description": "Documentation that simply works",
|
||||||
"html_url": "https://github.com/squidfunk/mkdocs-material",
|
"html_url": "https://github.com/squidfunk/mkdocs-material",
|
||||||
"language": "Python",
|
"language": "Python",
|
||||||
"stars_count": 26215,
|
"stars_count": 26208,
|
||||||
"forks_count": 4051,
|
"forks_count": 4048,
|
||||||
"open_issues_count": 2,
|
"open_issues_count": 2,
|
||||||
"updated_at": "2026-03-09T19:46:16Z",
|
"updated_at": "2026-03-08T17:43:54Z",
|
||||||
"created_at": "2016-01-28T22:09:23Z",
|
"created_at": "2016-01-28T22:09:23Z",
|
||||||
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
|
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
|
||||||
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
|
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -1,199 +0,0 @@
|
|||||||
"""
|
|
||||||
MkDocs Hook for Wiki-Link Resolution
|
|
||||||
|
|
||||||
Converts Obsidian-style [[wiki-links]] to standard Markdown links/images.
|
|
||||||
|
|
||||||
Supported syntax:
|
|
||||||
[[page-name]] → [page-name](../page-name/)
|
|
||||||
[[page-name|Display Text]] → [Display Text](../page-name/)
|
|
||||||
[[page-name#heading]] → [page-name](../page-name/#heading)
|
|
||||||
![[image.png]] → 
|
|
||||||
![[image.png|alt text]] → 
|
|
||||||
|
|
||||||
Links inside fenced code blocks and inline code are skipped.
|
|
||||||
Unresolved links are left as-is with a warning logged.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import logging
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Module-level file index: filename (lowercase) → relative path from docs root
|
|
||||||
_file_index: Dict[str, str] = {}
|
|
||||||
|
|
||||||
# Whether directory URLs are enabled
|
|
||||||
_use_directory_urls: bool = True
|
|
||||||
|
|
||||||
# Image extensions
|
|
||||||
_IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp', '.tiff', '.avif'}
|
|
||||||
|
|
||||||
|
|
||||||
def on_files(files: Any, config: Dict[str, Any]) -> None:
|
|
||||||
"""Build a file index mapping lowercased filenames to their paths."""
|
|
||||||
global _file_index, _use_directory_urls
|
|
||||||
_file_index.clear()
|
|
||||||
_use_directory_urls = config.get('use_directory_urls', True)
|
|
||||||
|
|
||||||
for f in files:
|
|
||||||
# f.src_path is relative to docs_dir, e.g. "docs/getting-started/installation.md"
|
|
||||||
src = f.src_path
|
|
||||||
basename = os.path.basename(src)
|
|
||||||
# For .md files, also index without extension
|
|
||||||
name_lower = basename.lower()
|
|
||||||
_file_index[name_lower] = src
|
|
||||||
|
|
||||||
if name_lower.endswith('.md'):
|
|
||||||
stem = name_lower[:-3]
|
|
||||||
# Don't overwrite if stem already exists (first wins)
|
|
||||||
if stem not in _file_index:
|
|
||||||
_file_index[stem] = src
|
|
||||||
|
|
||||||
logger.info(f"[wikilinks] Indexed {len(_file_index)} files for wiki-link resolution")
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_link(name: str) -> Optional[str]:
|
|
||||||
"""Look up a filename in the index (case-insensitive). Returns src_path or None."""
|
|
||||||
key = name.lower().strip()
|
|
||||||
return _file_index.get(key)
|
|
||||||
|
|
||||||
|
|
||||||
def _compute_relative_path(from_page_src: str, to_src: str, anchor: str = '') -> str:
|
|
||||||
"""Compute relative URL from one page to another, accounting for directory URLs."""
|
|
||||||
from_dir = os.path.dirname(from_page_src)
|
|
||||||
|
|
||||||
_, ext = os.path.splitext(to_src)
|
|
||||||
is_image = ext.lower() in _IMAGE_EXTENSIONS
|
|
||||||
is_md = ext.lower() == '.md'
|
|
||||||
|
|
||||||
if is_image:
|
|
||||||
# Images: direct relative path to the file
|
|
||||||
rel = os.path.relpath(to_src, from_dir)
|
|
||||||
return rel.replace(os.sep, '/')
|
|
||||||
|
|
||||||
if is_md and _use_directory_urls:
|
|
||||||
# With directory URLs, pages become page-name/index.html
|
|
||||||
# So we link to the directory (without .md)
|
|
||||||
page_dir = to_src[:-3] # strip .md
|
|
||||||
# Handle index.md → links to the directory itself
|
|
||||||
if os.path.basename(to_src).lower() == 'index.md':
|
|
||||||
page_dir = os.path.dirname(to_src)
|
|
||||||
rel = os.path.relpath(page_dir, from_dir)
|
|
||||||
url = rel.replace(os.sep, '/') + '/'
|
|
||||||
elif is_md:
|
|
||||||
# Without directory URLs, link directly to .md (MkDocs converts to .html)
|
|
||||||
rel = os.path.relpath(to_src, from_dir)
|
|
||||||
url = rel.replace(os.sep, '/').replace('.md', '.html')
|
|
||||||
else:
|
|
||||||
# Other files: direct path
|
|
||||||
rel = os.path.relpath(to_src, from_dir)
|
|
||||||
url = rel.replace(os.sep, '/')
|
|
||||||
|
|
||||||
if anchor:
|
|
||||||
url += '#' + anchor
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
|
|
||||||
# Regex to match wiki-links: optional ! prefix, then [[content]]
|
|
||||||
_WIKILINK_RE = re.compile(r'(!?)\[\[([^\]]+)\]\]')
|
|
||||||
|
|
||||||
|
|
||||||
def _replace_wikilinks(markdown: str, page_src: str) -> str:
|
|
||||||
"""Replace wiki-links in markdown, skipping code blocks."""
|
|
||||||
lines = markdown.split('\n')
|
|
||||||
result_lines = []
|
|
||||||
in_fenced_block = False
|
|
||||||
|
|
||||||
for line in lines:
|
|
||||||
# Track fenced code blocks (``` or ~~~)
|
|
||||||
stripped = line.lstrip()
|
|
||||||
if stripped.startswith('```') or stripped.startswith('~~~'):
|
|
||||||
in_fenced_block = not in_fenced_block
|
|
||||||
result_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if in_fenced_block:
|
|
||||||
result_lines.append(line)
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Process wiki-links in this line, but skip inline code
|
|
||||||
# Strategy: split by inline code spans, only process non-code parts
|
|
||||||
parts = re.split(r'(`[^`]+`)', line)
|
|
||||||
processed_parts = []
|
|
||||||
for part in parts:
|
|
||||||
if part.startswith('`') and part.endswith('`'):
|
|
||||||
# Inside inline code — leave as-is
|
|
||||||
processed_parts.append(part)
|
|
||||||
else:
|
|
||||||
# Process wiki-links in this segment
|
|
||||||
processed_parts.append(_WIKILINK_RE.sub(
|
|
||||||
lambda m: _resolve_wikilink(m, page_src),
|
|
||||||
part
|
|
||||||
))
|
|
||||||
result_lines.append(''.join(processed_parts))
|
|
||||||
|
|
||||||
return '\n'.join(result_lines)
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_wikilink(match: re.Match, page_src: str) -> str:
|
|
||||||
"""Resolve a single wiki-link match to markdown."""
|
|
||||||
is_embed = match.group(1) == '!'
|
|
||||||
inner = match.group(2).strip()
|
|
||||||
|
|
||||||
# Parse: name|display and name#anchor
|
|
||||||
display = None
|
|
||||||
anchor = ''
|
|
||||||
|
|
||||||
if '|' in inner:
|
|
||||||
name_part, display = inner.split('|', 1)
|
|
||||||
display = display.strip()
|
|
||||||
name_part = name_part.strip()
|
|
||||||
else:
|
|
||||||
name_part = inner
|
|
||||||
|
|
||||||
if '#' in name_part:
|
|
||||||
name_part, anchor = name_part.split('#', 1)
|
|
||||||
anchor = anchor.strip()
|
|
||||||
|
|
||||||
name_part = name_part.strip()
|
|
||||||
|
|
||||||
# Resolve the target file
|
|
||||||
target_src = _resolve_link(name_part)
|
|
||||||
|
|
||||||
if target_src is None:
|
|
||||||
# Try with .md appended
|
|
||||||
target_src = _resolve_link(name_part + '.md')
|
|
||||||
|
|
||||||
if target_src is None:
|
|
||||||
# Try common image extensions
|
|
||||||
for ext in ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']:
|
|
||||||
target_src = _resolve_link(name_part + ext)
|
|
||||||
if target_src:
|
|
||||||
break
|
|
||||||
|
|
||||||
if target_src is None:
|
|
||||||
logger.warning(f"[wikilinks] Unresolved wiki-link: [[{inner}]] in {page_src}")
|
|
||||||
return match.group(0) # Leave as-is
|
|
||||||
|
|
||||||
url = _compute_relative_path(page_src, target_src, anchor)
|
|
||||||
|
|
||||||
if is_embed:
|
|
||||||
# Image embed: ![[image.png]] or ![[image.png|alt text]]
|
|
||||||
alt = display or name_part
|
|
||||||
return f''
|
|
||||||
else:
|
|
||||||
# Document link: [[page]] or [[page|Display Text]]
|
|
||||||
label = display or (name_part + (f'#{anchor}' if anchor else ''))
|
|
||||||
return f'[{label}]({url})'
|
|
||||||
|
|
||||||
|
|
||||||
def on_page_markdown(markdown: str, page: Any, config: Dict[str, Any], files: Any) -> str:
|
|
||||||
"""Process wiki-links in page markdown content."""
|
|
||||||
# Quick check — skip pages without wiki-links
|
|
||||||
if '[[' not in markdown:
|
|
||||||
return markdown
|
|
||||||
|
|
||||||
return _replace_wikilinks(markdown, page.file.src_path)
|
|
||||||
@ -43,12 +43,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -112,20 +106,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -229,96 +209,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -561,124 +451,5 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
{% if config.theme.palette %}
|
|
||||||
{% if not config.theme.palette is mapping %}
|
|
||||||
{% include "partials/palette.html" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if "material/search" in config.plugins %}
|
|
||||||
{% include "partials/search.html" %}
|
|
||||||
{% endif %}
|
|
||||||
</header>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block tabs %}{% endblock %}
|
|
||||||
|
|||||||
@ -5,10 +5,3 @@ hide:
|
|||||||
- toc
|
- toc
|
||||||
title: "Test Page"
|
title: "Test Page"
|
||||||
---
|
---
|
||||||
---
|
|
||||||
template: test-page.html
|
|
||||||
hide:
|
|
||||||
- navigation
|
|
||||||
- toc
|
|
||||||
title: "Test Page"
|
|
||||||
---
|
|
||||||
|
|||||||
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
Testing page.
|
Testing page.
|
||||||
|
|
||||||
[[test-page]]]]
|
|
||||||
|
|
||||||
|
|
||||||
<div class="photo-block" data-photo-id="1" data-size="large" data-caption="" data-link-to-gallery="true" data-alignment="center">Loading...</div>
|
<div class="photo-block" data-photo-id="1" data-size="large" data-caption="" data-link-to-gallery="true" data-alignment="center">Loading...</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,19 +1,2 @@
|
|||||||
# testing
|
# testing
|
||||||
# testing
|
# testing
|
||||||
|
|
||||||
## Wiki-Link Tests
|
|
||||||
|
|
||||||
- Doc link: [[installation]]
|
|
||||||
- Doc link with display text: [[installation|Install Guide]]
|
|
||||||
- Doc link with anchor: [[installation#prerequisites]]
|
|
||||||
- Image embed: ![[logo.png]]
|
|
||||||
- Image with alt: ![[logo.png|Site Logo]]
|
|
||||||
- Code block (should NOT be converted):
|
|
||||||
|
|
||||||
```
|
|
||||||
[[this-should-stay-as-is]]
|
|
||||||
```
|
|
||||||
|
|
||||||
- Inline code (should NOT be converted): `[[not-a-link]]`
|
|
||||||
- Unresolved link (should stay as-is): [[nonexistent-page]]
|
|
||||||
|
|
||||||
|
|||||||
@ -94,7 +94,6 @@ extra_javascript:
|
|||||||
hooks:
|
hooks:
|
||||||
- docs/hooks/repo_widget_hook.py
|
- docs/hooks/repo_widget_hook.py
|
||||||
- docs/hooks/env_config_hook.py
|
- docs/hooks/env_config_hook.py
|
||||||
- docs/hooks/wikilinks_hook.py
|
|
||||||
|
|
||||||
# Markdown Extensions
|
# Markdown Extensions
|
||||||
markdown_extensions:
|
markdown_extensions:
|
||||||
|
|||||||
@ -200,12 +200,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -269,20 +263,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -386,96 +366,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -718,108 +608,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -831,8 +619,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="/." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="/assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -860,9 +677,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -902,11 +728,105 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="/." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="/docs/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="/blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -7,10 +7,10 @@
|
|||||||
"stars_count": 0,
|
"stars_count": 0,
|
||||||
"forks_count": 0,
|
"forks_count": 0,
|
||||||
"open_issues_count": 23,
|
"open_issues_count": 23,
|
||||||
"updated_at": "2026-03-09T12:23:17-06:00",
|
"updated_at": "2026-03-08T18:11:30-06:00",
|
||||||
"created_at": "2025-05-28T14:54:59-06:00",
|
"created_at": "2025-05-28T14:54:59-06:00",
|
||||||
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
|
"clone_url": "https://gitea.bnkops.com/admin/changemaker.lite.git",
|
||||||
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
|
"ssh_url": "git@gitea.bnkops.com:admin/changemaker.lite.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T12:23:17-06:00"
|
"last_build_update": "2026-03-08T18:11:30-06:00"
|
||||||
}
|
}
|
||||||
@ -4,10 +4,10 @@
|
|||||||
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
|
"description": "Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.",
|
||||||
"html_url": "https://github.com/anthropics/claude-code",
|
"html_url": "https://github.com/anthropics/claude-code",
|
||||||
"language": "Shell",
|
"language": "Shell",
|
||||||
"stars_count": 75798,
|
"stars_count": 75344,
|
||||||
"forks_count": 6114,
|
"forks_count": 6074,
|
||||||
"open_issues_count": 5868,
|
"open_issues_count": 5793,
|
||||||
"updated_at": "2026-03-09T21:59:41Z",
|
"updated_at": "2026-03-09T00:15:16Z",
|
||||||
"created_at": "2025-02-22T17:41:21Z",
|
"created_at": "2025-02-22T17:41:21Z",
|
||||||
"clone_url": "https://github.com/anthropics/claude-code.git",
|
"clone_url": "https://github.com/anthropics/claude-code.git",
|
||||||
"ssh_url": "git@github.com:anthropics/claude-code.git",
|
"ssh_url": "git@github.com:anthropics/claude-code.git",
|
||||||
|
|||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "VS Code in the browser",
|
"description": "VS Code in the browser",
|
||||||
"html_url": "https://github.com/coder/code-server",
|
"html_url": "https://github.com/coder/code-server",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 76554,
|
"stars_count": 76540,
|
||||||
"forks_count": 6541,
|
"forks_count": 6539,
|
||||||
"open_issues_count": 169,
|
"open_issues_count": 169,
|
||||||
"updated_at": "2026-03-09T20:21:15Z",
|
"updated_at": "2026-03-08T21:32:19Z",
|
||||||
"created_at": "2019-02-27T16:50:41Z",
|
"created_at": "2019-02-27T16:50:41Z",
|
||||||
"clone_url": "https://github.com/coder/code-server.git",
|
"clone_url": "https://github.com/coder/code-server.git",
|
||||||
"ssh_url": "git@github.com:coder/code-server.git",
|
"ssh_url": "git@github.com:coder/code-server.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T19:30:51Z"
|
"last_build_update": "2026-03-06T12:59:10Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
|
"description": "A highly customizable homepage (or startpage / application dashboard) with Docker and service API integrations.",
|
||||||
"html_url": "https://github.com/gethomepage/homepage",
|
"html_url": "https://github.com/gethomepage/homepage",
|
||||||
"language": "JavaScript",
|
"language": "JavaScript",
|
||||||
"stars_count": 28818,
|
"stars_count": 28793,
|
||||||
"forks_count": 1812,
|
"forks_count": 1811,
|
||||||
"open_issues_count": 1,
|
"open_issues_count": 1,
|
||||||
"updated_at": "2026-03-09T21:38:58Z",
|
"updated_at": "2026-03-08T23:44:14Z",
|
||||||
"created_at": "2022-08-24T07:29:42Z",
|
"created_at": "2022-08-24T07:29:42Z",
|
||||||
"clone_url": "https://github.com/gethomepage/homepage.git",
|
"clone_url": "https://github.com/gethomepage/homepage.git",
|
||||||
"ssh_url": "git@github.com:gethomepage/homepage.git",
|
"ssh_url": "git@github.com:gethomepage/homepage.git",
|
||||||
"default_branch": "dev",
|
"default_branch": "dev",
|
||||||
"last_build_update": "2026-03-09T17:02:06Z"
|
"last_build_update": "2026-03-08T12:16:51Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
"description": "Git with a cup of tea! Painless self-hosted all-in-one software development service, including Git hosting, code review, team collaboration, package registry and CI/CD",
|
||||||
"html_url": "https://github.com/go-gitea/gitea",
|
"html_url": "https://github.com/go-gitea/gitea",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 54194,
|
"stars_count": 54180,
|
||||||
"forks_count": 6448,
|
"forks_count": 6438,
|
||||||
"open_issues_count": 2850,
|
"open_issues_count": 2846,
|
||||||
"updated_at": "2026-03-09T21:55:30Z",
|
"updated_at": "2026-03-08T23:25:42Z",
|
||||||
"created_at": "2016-11-01T02:13:26Z",
|
"created_at": "2016-11-01T02:13:26Z",
|
||||||
"clone_url": "https://github.com/go-gitea/gitea.git",
|
"clone_url": "https://github.com/go-gitea/gitea.git",
|
||||||
"ssh_url": "git@github.com:go-gitea/gitea.git",
|
"ssh_url": "git@github.com:go-gitea/gitea.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T10:12:24Z"
|
"last_build_update": "2026-03-08T20:49:59Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
|
"description": "High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app.",
|
||||||
"html_url": "https://github.com/knadh/listmonk",
|
"html_url": "https://github.com/knadh/listmonk",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 19236,
|
"stars_count": 19221,
|
||||||
"forks_count": 1945,
|
"forks_count": 1945,
|
||||||
"open_issues_count": 99,
|
"open_issues_count": 99,
|
||||||
"updated_at": "2026-03-09T21:14:22Z",
|
"updated_at": "2026-03-08T23:08:55Z",
|
||||||
"created_at": "2019-06-26T05:08:39Z",
|
"created_at": "2019-06-26T05:08:39Z",
|
||||||
"clone_url": "https://github.com/knadh/listmonk.git",
|
"clone_url": "https://github.com/knadh/listmonk.git",
|
||||||
"ssh_url": "git@github.com:knadh/listmonk.git",
|
"ssh_url": "git@github.com:knadh/listmonk.git",
|
||||||
"default_branch": "master",
|
"default_branch": "master",
|
||||||
"last_build_update": "2026-03-09T03:05:44Z"
|
"last_build_update": "2026-03-08T14:04:45Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
|
"description": "Create & scan cute qr codes easily \ud83d\udc7e",
|
||||||
"html_url": "https://github.com/lyqht/mini-qr",
|
"html_url": "https://github.com/lyqht/mini-qr",
|
||||||
"language": "Vue",
|
"language": "Vue",
|
||||||
"stars_count": 1897,
|
"stars_count": 1898,
|
||||||
"forks_count": 241,
|
"forks_count": 240,
|
||||||
"open_issues_count": 22,
|
"open_issues_count": 21,
|
||||||
"updated_at": "2026-03-09T15:15:51Z",
|
"updated_at": "2026-03-08T15:02:09Z",
|
||||||
"created_at": "2023-04-21T14:20:14Z",
|
"created_at": "2023-04-21T14:20:14Z",
|
||||||
"clone_url": "https://github.com/lyqht/mini-qr.git",
|
"clone_url": "https://github.com/lyqht/mini-qr.git",
|
||||||
"ssh_url": "git@github.com:lyqht/mini-qr.git",
|
"ssh_url": "git@github.com:lyqht/mini-qr.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T07:31:47Z"
|
"last_build_update": "2026-03-05T13:18:42Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
|
"description": "Fair-code workflow automation platform with native AI capabilities. Combine visual building with custom code, self-host or cloud, 400+ integrations.",
|
||||||
"html_url": "https://github.com/n8n-io/n8n",
|
"html_url": "https://github.com/n8n-io/n8n",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 178343,
|
"stars_count": 178152,
|
||||||
"forks_count": 55592,
|
"forks_count": 55558,
|
||||||
"open_issues_count": 1414,
|
"open_issues_count": 1405,
|
||||||
"updated_at": "2026-03-09T21:52:13Z",
|
"updated_at": "2026-03-09T00:14:18Z",
|
||||||
"created_at": "2019-06-22T09:24:21Z",
|
"created_at": "2019-06-22T09:24:21Z",
|
||||||
"clone_url": "https://github.com/n8n-io/n8n.git",
|
"clone_url": "https://github.com/n8n-io/n8n.git",
|
||||||
"ssh_url": "git@github.com:n8n-io/n8n.git",
|
"ssh_url": "git@github.com:n8n-io/n8n.git",
|
||||||
"default_branch": "master",
|
"default_branch": "master",
|
||||||
"last_build_update": "2026-03-09T21:33:49Z"
|
"last_build_update": "2026-03-09T00:10:51Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
|
"description": "\ud83d\udd25 \ud83d\udd25 \ud83d\udd25 A Free & Self-hostable Airtable Alternative",
|
||||||
"html_url": "https://github.com/nocodb/nocodb",
|
"html_url": "https://github.com/nocodb/nocodb",
|
||||||
"language": "TypeScript",
|
"language": "TypeScript",
|
||||||
"stars_count": 62431,
|
"stars_count": 62384,
|
||||||
"forks_count": 4659,
|
"forks_count": 4655,
|
||||||
"open_issues_count": 629,
|
"open_issues_count": 627,
|
||||||
"updated_at": "2026-03-09T19:42:51Z",
|
"updated_at": "2026-03-08T23:36:57Z",
|
||||||
"created_at": "2017-10-29T18:51:48Z",
|
"created_at": "2017-10-29T18:51:48Z",
|
||||||
"clone_url": "https://github.com/nocodb/nocodb.git",
|
"clone_url": "https://github.com/nocodb/nocodb.git",
|
||||||
"ssh_url": "git@github.com:nocodb/nocodb.git",
|
"ssh_url": "git@github.com:nocodb/nocodb.git",
|
||||||
"default_branch": "develop",
|
"default_branch": "develop",
|
||||||
"last_build_update": "2026-03-09T12:25:14Z"
|
"last_build_update": "2026-03-07T10:48:26Z"
|
||||||
}
|
}
|
||||||
@ -4,13 +4,13 @@
|
|||||||
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
|
"description": "Get up and running with Kimi-K2.5, GLM-5, MiniMax, DeepSeek, gpt-oss, Qwen, Gemma and other models.",
|
||||||
"html_url": "https://github.com/ollama/ollama",
|
"html_url": "https://github.com/ollama/ollama",
|
||||||
"language": "Go",
|
"language": "Go",
|
||||||
"stars_count": 164642,
|
"stars_count": 164479,
|
||||||
"forks_count": 14860,
|
"forks_count": 14834,
|
||||||
"open_issues_count": 2609,
|
"open_issues_count": 2613,
|
||||||
"updated_at": "2026-03-09T21:37:49Z",
|
"updated_at": "2026-03-09T00:13:40Z",
|
||||||
"created_at": "2023-06-26T19:39:32Z",
|
"created_at": "2023-06-26T19:39:32Z",
|
||||||
"clone_url": "https://github.com/ollama/ollama.git",
|
"clone_url": "https://github.com/ollama/ollama.git",
|
||||||
"ssh_url": "git@github.com:ollama/ollama.git",
|
"ssh_url": "git@github.com:ollama/ollama.git",
|
||||||
"default_branch": "main",
|
"default_branch": "main",
|
||||||
"last_build_update": "2026-03-09T21:19:34Z"
|
"last_build_update": "2026-03-08T06:32:28Z"
|
||||||
}
|
}
|
||||||
@ -4,10 +4,10 @@
|
|||||||
"description": "Documentation that simply works",
|
"description": "Documentation that simply works",
|
||||||
"html_url": "https://github.com/squidfunk/mkdocs-material",
|
"html_url": "https://github.com/squidfunk/mkdocs-material",
|
||||||
"language": "Python",
|
"language": "Python",
|
||||||
"stars_count": 26215,
|
"stars_count": 26208,
|
||||||
"forks_count": 4051,
|
"forks_count": 4048,
|
||||||
"open_issues_count": 2,
|
"open_issues_count": 2,
|
||||||
"updated_at": "2026-03-09T19:46:16Z",
|
"updated_at": "2026-03-08T17:43:54Z",
|
||||||
"created_at": "2016-01-28T22:09:23Z",
|
"created_at": "2016-01-28T22:09:23Z",
|
||||||
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
|
"clone_url": "https://github.com/squidfunk/mkdocs-material.git",
|
||||||
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
|
"ssh_url": "git@github.com:squidfunk/mkdocs-material.git",
|
||||||
|
|||||||
@ -222,12 +222,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -291,20 +285,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -408,96 +388,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -740,108 +630,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -853,8 +641,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href=".." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -882,9 +699,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -924,11 +750,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href=".." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../docs/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="./" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -215,12 +215,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -284,20 +278,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -401,96 +381,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -733,108 +623,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -846,8 +634,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Signing in...
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -875,9 +692,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -917,11 +743,105 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../docs/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Advocacy Campaigns
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Email Queue
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Advocacy
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Representatives
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Response Moderation
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Email Templates
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Broadcast
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Newsletter (Listmonk)
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
SMS Campaigns
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Dashboard
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Admin Guide
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
@ -224,12 +224,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span class="cm-header-nav__label">Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
<a href="/" class="cm-header-nav__link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span class="cm-header-nav__label">Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span class="cm-header-nav__label">Docs</span></a>
|
||||||
<label for="__search" class="cm-header-nav__utility" title="Search">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__utility" id="cm-palette-toggle" title="Toggle dark mode" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
</button>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__link" id="cm-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span class="cm-header-nav__label">Sign In</span>
|
<span class="cm-header-nav__label">Sign In</span>
|
||||||
@ -293,20 +287,6 @@
|
|||||||
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
<a href="#" data-path="/pages" class="cm-header-nav__mobile-link" data-nav-id="pages"><span class="material-icons-outlined">description</span><span>Pages</span></a>
|
||||||
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
<a href="/" class="cm-header-nav__mobile-link" data-nav-id="landing"><span class="material-icons-outlined">language</span><span>Website</span></a>
|
||||||
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
<a href="/docs/" class="cm-header-nav__mobile-link" data-nav-id="docs"><span class="material-icons-outlined">menu_book</span><span>Docs</span></a>
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<label for="__search" class="cm-header-nav__mobile-link" style="cursor:pointer">
|
|
||||||
<span class="material-icons-outlined">search</span>
|
|
||||||
<span>Search</span>
|
|
||||||
</label>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-mobile-palette-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">dark_mode</span>
|
|
||||||
<span>Dark Mode</span>
|
|
||||||
</button>
|
|
||||||
<button class="cm-header-nav__mobile-link cm-header-nav__utility-btn" id="cm-docs-sidebar-toggle" type="button">
|
|
||||||
<span class="material-icons-outlined">menu_book</span>
|
|
||||||
<span>Docs Navigation</span>
|
|
||||||
</button>
|
|
||||||
<div class="cm-header-nav__mobile-divider"></div>
|
|
||||||
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
<a href="#" data-path="/login" class="cm-header-nav__mobile-link" id="cm-mobile-signin-link">
|
||||||
<span class="material-icons-outlined">login</span>
|
<span class="material-icons-outlined">login</span>
|
||||||
<span>Sign In</span>
|
<span>Sign In</span>
|
||||||
@ -410,96 +390,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
document.body.appendChild(iframe);
|
document.body.appendChild(iframe);
|
||||||
// Palette toggle (dark/light mode)
|
|
||||||
function togglePalette() {
|
|
||||||
var inputs = document.querySelectorAll('.cm-palette-container input[name="__palette"]');
|
|
||||||
for (var i = 0; i < inputs.length; i++) {
|
|
||||||
if (!inputs[i].checked) { inputs[i].click(); break; }
|
|
||||||
}
|
|
||||||
setTimeout(updatePaletteIcon, 50);
|
|
||||||
}
|
|
||||||
function updatePaletteIcon() {
|
|
||||||
var scheme = document.body.getAttribute('data-md-color-scheme') || 'default';
|
|
||||||
var isDark = scheme === 'slate';
|
|
||||||
var icon = isDark ? 'light_mode' : 'dark_mode';
|
|
||||||
document.querySelectorAll('#cm-palette-toggle .material-icons-outlined, #cm-mobile-palette-toggle .material-icons-outlined').forEach(function(el) {
|
|
||||||
el.textContent = icon;
|
|
||||||
});
|
|
||||||
var ml = document.querySelector('#cm-mobile-palette-toggle span:not(.material-icons-outlined)');
|
|
||||||
if (ml) ml.textContent = isDark ? 'Light Mode' : 'Dark Mode';
|
|
||||||
}
|
|
||||||
var ptBtn = document.getElementById('cm-palette-toggle');
|
|
||||||
var ptBtnM = document.getElementById('cm-mobile-palette-toggle');
|
|
||||||
if (ptBtn) ptBtn.addEventListener('click', togglePalette);
|
|
||||||
if (ptBtnM) ptBtnM.addEventListener('click', function() { togglePalette(); closeDrawer(); });
|
|
||||||
// Docs sidebar toggle (opens Material's docs navigation drawer)
|
|
||||||
var docsSidebarBtn = document.getElementById('cm-docs-sidebar-toggle');
|
|
||||||
if (docsSidebarBtn) {
|
|
||||||
docsSidebarBtn.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
var dt = document.getElementById('__drawer');
|
|
||||||
if (dt) { dt.checked = !dt.checked; dt.dispatchEvent(new Event('change')); }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Close custom drawer when search label is clicked on mobile + auto-focus input
|
|
||||||
document.querySelectorAll('label[for="__search"]').forEach(function(el) {
|
|
||||||
el.addEventListener('click', function() {
|
|
||||||
closeDrawer();
|
|
||||||
setTimeout(function() {
|
|
||||||
var input = document.querySelector('.md-search__input');
|
|
||||||
if (input) input.focus();
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// Search activation: mirror checkbox state as a body class for CSS targeting.
|
|
||||||
// On desktop, Material's search input is always visible (overflow from collapsed
|
|
||||||
// header). Typing directly into it triggers the search worker but never checks
|
|
||||||
// the __search checkbox, so the results panel stays hidden. We fix this by
|
|
||||||
// checking the checkbox on input focus/input events.
|
|
||||||
var searchToggle = document.getElementById('__search');
|
|
||||||
if (searchToggle) {
|
|
||||||
function syncSearchClass() {
|
|
||||||
document.body.classList.toggle('cm-search-active', searchToggle.checked);
|
|
||||||
}
|
|
||||||
searchToggle.addEventListener('change', syncSearchClass);
|
|
||||||
syncSearchClass();
|
|
||||||
// Activate search when the Material input is focused or typed into directly.
|
|
||||||
// Uses event delegation because the search input is rendered after this
|
|
||||||
// script (announce block runs before header block).
|
|
||||||
document.addEventListener('focusin', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.addEventListener('input', function(e) {
|
|
||||||
if (e.target && e.target.classList && e.target.classList.contains('md-search__input')) {
|
|
||||||
if (!searchToggle.checked) {
|
|
||||||
searchToggle.checked = true;
|
|
||||||
searchToggle.dispatchEvent(new Event('change'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Click-outside to dismiss search
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!searchToggle.checked) return;
|
|
||||||
var panel = document.querySelector('.md-search__inner');
|
|
||||||
if (panel && panel.contains(e.target)) return;
|
|
||||||
if (e.target.closest && e.target.closest('label[for="__search"]')) return;
|
|
||||||
searchToggle.checked = false;
|
|
||||||
syncSearchClass();
|
|
||||||
});
|
|
||||||
// Also sync on Escape key (Material toggles checkbox via JS)
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.key === 'Escape') setTimeout(syncSearchClass, 50);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Init palette icon + observe changes
|
|
||||||
setTimeout(updatePaletteIcon, 100);
|
|
||||||
new MutationObserver(function() { updatePaletteIcon(); })
|
|
||||||
.observe(document.body, { attributes: true, attributeFilter: ['data-md-color-scheme'] });
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
@ -742,108 +632,6 @@
|
|||||||
.cm-header-nav__hamburger { display: block; }
|
.cm-header-nav__hamburger { display: block; }
|
||||||
.cm-header-nav__dropdown-menu { display: none !important; }
|
.cm-header-nav__dropdown-menu { display: none !important; }
|
||||||
}
|
}
|
||||||
/* Hidden Material header — stays at 0 height normally */
|
|
||||||
.md-header--cm-hidden {
|
|
||||||
height: 0 !important;
|
|
||||||
min-height: 0 !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
background: transparent !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === DESKTOP SEARCH (>= 60em / 960px) === */
|
|
||||||
@media screen and (min-width: 60em) {
|
|
||||||
/* When search is active, make the search panel a fixed dropdown below custom header */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 56px !important;
|
|
||||||
right: 16px !important;
|
|
||||||
left: auto !important;
|
|
||||||
width: min(34rem, calc(100vw - 32px)) !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
border-radius: 0 0 8px 8px !important;
|
|
||||||
box-shadow: 0 4px 24px rgba(0,0,0,0.25) !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark overlay behind search panel */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__overlay {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
width: 100vw !important;
|
|
||||||
height: 100vh !important;
|
|
||||||
background: rgba(0,0,0,0.54) !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
z-index: 299 !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
transform: none !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === MOBILE SEARCH (< 60em / 960px) === */
|
|
||||||
@media screen and (max-width: 59.984375em) {
|
|
||||||
/* Full-screen search takeover on mobile */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__inner {
|
|
||||||
position: fixed !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
width: 100% !important;
|
|
||||||
height: 100% !important;
|
|
||||||
opacity: 1 !important;
|
|
||||||
overflow: visible !important;
|
|
||||||
transform: none !important;
|
|
||||||
z-index: 300 !important;
|
|
||||||
background: var(--md-default-bg-color) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Force search results to show when active (both breakpoints) */
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__output {
|
|
||||||
opacity: 1 !important;
|
|
||||||
}
|
|
||||||
body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|
||||||
max-height: 75vh !important;
|
|
||||||
}
|
|
||||||
.cm-palette-container {
|
|
||||||
height: 0 !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
}
|
|
||||||
/* Hide Material tabs — custom header covers navigation */
|
|
||||||
.md-tabs { display: none !important; }
|
|
||||||
/* Utility icon styling */
|
|
||||||
.cm-header-nav__utility {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 4px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
.cm-header-nav__utility:hover { color: #fff; }
|
|
||||||
.cm-header-nav__utility .material-icons-outlined { font-size: 20px; }
|
|
||||||
.cm-header-nav__utility-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: rgba(255,255,255,0.85);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 15px;
|
|
||||||
font-family: inherit;
|
|
||||||
width: 100%;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.cm-header-nav__mobile-divider {
|
|
||||||
height: 1px;
|
|
||||||
background: rgba(255,255,255,0.1);
|
|
||||||
margin: 8px 24px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -855,8 +643,37 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<header class="md-header md-header--cm-hidden" data-md-component="header">
|
|
||||||
<div class="cm-palette-container">
|
|
||||||
|
|
||||||
|
|
||||||
|
<header class="md-header md-header--shadow md-header--lifted" data-md-component="header">
|
||||||
|
<nav class="md-header__inner md-grid" aria-label="Header">
|
||||||
|
<a href="../../../.." title="Changemaker Lite" class="md-header__button md-logo" aria-label="Changemaker Lite" data-md-component="logo">
|
||||||
|
|
||||||
|
<img src="../../../../assets/logo.png" alt="logo">
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<label class="md-header__button md-icon" for="__drawer">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-header__title" data-md-component="header-title">
|
||||||
|
<div class="md-header__ellipsis">
|
||||||
|
<div class="md-header__topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
Changemaker Lite
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="md-header__topic" data-md-component="header-topic">
|
||||||
|
<span class="md-ellipsis">
|
||||||
|
|
||||||
|
Areas (Cuts)
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<form class="md-header__option" data-md-component="palette">
|
<form class="md-header__option" data-md-component="palette">
|
||||||
@ -884,9 +701,18 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md-search" data-md-component="search" role="dialog">
|
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<label class="md-header__button md-icon" for="__search">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||||
|
</label>
|
||||||
|
<div class="md-search" data-md-component="search" role="dialog">
|
||||||
<label class="md-search__overlay" for="__search"></label>
|
<label class="md-search__overlay" for="__search"></label>
|
||||||
<div class="md-search__inner" role="search">
|
<div class="md-search__inner" role="search">
|
||||||
<form class="md-search__form" name="search">
|
<form class="md-search__form" name="search">
|
||||||
@ -926,11 +752,107 @@ body.cm-search-active .md-header--cm-hidden .md-search__scrollwrap {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="md-header__source">
|
||||||
|
<a href="https://gitea.bnkops.com/admin/changemaker.lite" title="Go to repository" class="md-source" data-md-component="source">
|
||||||
|
<div class="md-source__icon md-icon">
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="md-source__repository">
|
||||||
|
changemaker.lite
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="md-tabs" aria-label="Tabs" data-md-component="tabs">
|
||||||
|
<div class="md-grid">
|
||||||
|
<ul class="md-tabs__list">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../.." class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Home
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item md-tabs__item--active">
|
||||||
|
<a href="../../../" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5c-1.35-.85-3.8-1.5-5.5-1.5-1.65 0-3.35.3-4.75 1.05-.1.05-.15.05-.25.05-.25 0-.5-.25-.5-.5V6c.6-.45 1.25-.75 2-1 1.11-.35 2.33-.5 3.5-.5 1.95 0 4.05.4 5.5 1.5 1.45-1.1 3.55-1.5 5.5-1.5 1.17 0 2.39.15 3.5.5.75.25 1.4.55 2 1v14.6c0 .25-.25.5-.5.5-.1 0-.15 0-.25-.05-1.4-.75-3.1-1.05-4.75-1.05-1.7 0-4.15.65-5.5 1.5M12 8v11.5c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5V7c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5m1 3.5c1.11-.68 2.6-1 4.5-1 .91 0 1.76.09 2.5.28V9.23c-.87-.15-1.71-.23-2.5-.23q-2.655 0-4.5.84zm4.5.17c-1.71 0-3.21.26-4.5.79v1.69c1.11-.65 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24v-1.5c-.87-.16-1.71-.23-2.5-.23m2.5 2.9c-.87-.16-1.71-.24-2.5-.24-1.83 0-3.33.27-4.5.8v1.69c1.11-.66 2.6-.99 4.5-.99 1.04 0 1.88.08 2.5.24z"/></svg>
|
||||||
|
|
||||||
|
|
||||||
|
Docs
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<li class="md-tabs__item">
|
||||||
|
<a href="../../../../blog/" class="md-tabs__link">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Blog
|
||||||
|
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="md-container" data-md-component="container">
|
<div class="md-container" data-md-component="container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<main class="md-main" data-md-component="main">
|
<main class="md-main" data-md-component="main">
|
||||||
<div class="md-main__inner md-grid">
|
<div class="md-main__inner md-grid">
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user