Profile
Back to NewsBack
GitHub Trending 3 min
Reader Mode
RobocopMao/r-markdown: 一款专为微信公众号设计的 Markdown 实时预览编辑器,支持丰富的扩展排版组件和一键复制到公众号。

RobocopMao/r-markdown: 一款专为微信公众号设计的 Markdown 实时预览编辑器,支持丰富的扩展排版组件和一键复制到公众号。

10 hours ago

R-Markdown 编辑器

专为微信公众号打造的 Markdown 排版工具,所见即所得,一键复制到公众号后台。
同时提供 macOS / Windows 桌面客户端,本地离线使用。

当前版本 0.4.7。

✨ 功能特性

写作与排版

  • 实时预览 — 左侧编辑 Markdown,右侧实时渲染公众号效果
  • 一键复制 — 富文本 / HTML 源码两种复制模式,直接粘贴到公众号编辑器
  • 多格式导出 — 保存图片(PNG)、小红书图(首图 + 正文卡片,桌面端打包 ZIP)、PDF、HTML
  • 主题切换 — 15 款预设主题色 + 自定义颜色,支持暗色模式
  • 编辑器配色 — 10 套代码高亮主题(默认 / GitHub / Solarized / Material / One Dark / Dracula / Monokai)
  • 段落格式 — 字号、字重、行高、段间距、首行缩进可调
  • 滚动同步 — 编辑器与预览面板滚动位置按比例联动
  • 查找替换 — 支持区分大小写、全词匹配、正则表达式,含上一个 / 下一个 / 全部替换
  • 文档大纲 — 右侧大纲面板汇总标题层级,点击跳转并高亮定位
  • 底部状态栏 — 显示光标行列、已选字数、总字数与预估阅读时长
  • 命令面板 — ⌘K / Ctrl+K 统一检索草稿 / 云文章 / 本地文章 / 素材,含最近搜索
  • AI 写作 — 内置 khazix-writer 写作技能(数字生命卡兹克的公众号长文写法),以对话方式从素材聊到成稿:输入框固定在面板底部,第一条消息即素材(brief / 链接 / 语音转文字 / 零散想法),支持「用当前文档」「选择文件」「填经历模板」三个快捷入口,素材不足时模型会先追问而不是硬写。出稿后按 Markdown 渲染为稿件卡,历次稿件都留在对话里可回看、可应用;输入框上方的参数行有一个「卡兹克写作技能」勾选框(默认开启),它一并控制技能本体、参考资料(风格示例 + 内容方法论,约 +6900 tokens)与出稿后的四层自检(L1 硬性规则 / L2 风格一致性 / L3 内容质量 / L4 活人感终审,报告可折叠,可一键「按自检意见改稿」);取消勾选即整套技能都不下发,模型自由发挥。单会话 + 保留时长滑动续期(默认 24 小时,从最后一次请求算起,可单独调),对话与稿件存本地 IndexedDB,超时或点「新建会话」即清空。与「AI 排版」共用同一份接口配置(「设置 → AI 设置 → AI 文本」里的接口地址 / API Key / 模型名),另有写作专属的采样温度 / 最大输出 / 结果保留时长 / 长期补充要求。桌面端与移动端均可使用
  • AI 排版 — 接入任意 OpenAI 兼容接口(自填地址 / API Key / 模型名),一键把文章转成 R-Markdown 排版稿;支持当前文档、粘贴文本、选择文件(.md / .txt / .docx)三种来源,结果以差异对比展示(桌面端左右双栏、移动端单栏逐行),结果本地保留 24 小时(时长可在设置里单独调,从最后一次请求算起,面板顶部显示倒计时,可随时清空),确认后再替换编辑器内容(可撤销);「补充要求」可存为常用项勾选复用,勾选状态持久化,并带全选 / 全不选。桌面端与移动端均可使用
  • AI 配图 — 提示词直接出配图,默认接入 Agnes(agnes-image-2.5-flash,当前免费);支持 1K–4K 尺寸档位与 8 种宽高比,出图后可预览、下载、在历史里回看;结果本地保留 24 小时(时长可在设置里单独调,从最后一次请求算起,面板顶部显示倒计时,可随时清空)。可用「优化提示词」把一句话交给大模型扩写成完整提示词(复用「AI 文本」配置的接口),「用标题」取文章标题当起点。放图进文章有三条路:「本地插入编辑器」走「粘贴/拖拽上传方式」的同一条链路(本地存储 / 本地磁盘 / GitHub 图床 / 乐塔图床,与手动粘贴图片完全一致),「上传图床并插入」直接传到图床用公开链接,桌面客户端另有「保存磁盘并插入」一步存到文章目录并插入。配置按厂商分开存放,新增厂商只需在 src/services/imageProviders.ts 加一条描述
  • 违禁词检测 — 五类内置词库(极限用语 / 权威背书 / 医疗功效 / 金融风险 / 诱导互动),支持自定义词与白名单,命中可点击跳转定位
  • 可调面板 — 拖拽调整编辑器与预览区宽度,工具栏按可用宽度自适应换行
  • 组件展示 — 内置排版组件库,可视化浏览所有可用组件及效果

内容管理

  • 自动保存 — 内容实时保存到 localStorage,刷新不丢失
  • 草稿管理 — IndexedDB 多草稿存储,支持导入 / 关联 / 自动匹配
  • 文章工作区 — 多仓库 / 多分支 / 多目录并存,下拉快速切换,单工作区自动设为「当前」
  • 云端文章 — 基于 GitHub 仓库的文章树管理(CRUD、拖拽、缓存、自动关联)
  • 本地文章 — 桌面端可将文章树存到本地磁盘(目录可配置、可迁移、重装后可重新加载)
  • 文章文件命名 — 可选「用菜单树文章名命名文件」(如 公众号排版技巧-k3f9a2b1.md),短哈希防重名;默认关闭,开启后重命名文章会同步改名
  • 图床上传 — 支持 IndexedDB / 本地磁盘 / GitHub / 乐塔四种图片存放方式
  • 图库 — 可视化浏览已上传图片,磁盘图片 hover 显示文件夹图标可定位本地文件
  • 素材库 — 可视化浏览 / 安装 / 发布排版素材,支持「我的素材」与官方素材库
  • 导入 — 支持 .md / .txt / .docx(docx 走 mammoth 提取纯文本)
  • 微信发布 — 直接上传草稿到微信公众号素材库(桌面端)

客户端

  • 桌面客户端 — 基于 Tauri 2 的 macOS(Apple Silicon)和 Windows(x64)原生应用
  • 自动更新 — 桌面客户端启动时自动检查新版本,一键下载安装
  • 页面缩放 — 桌面端支持整体缩放比例设置

🎨 排版能力

基于 awesome-design-md 排版引擎,支持丰富的公众号扩展语法:

内置的「AI 排版」所用的组件规范与 R-Markdown Formatter Skill 同源(src/data/ai/r-markdown-formatter/),因此 AI 生成的排版稿与手动排版结果遵循同一套组件标准。
> 内置的「AI 写作」所用的技能文本与 khazix-writer skill 同源(src/data/ai/khazix/),技能更新后手动重拷覆盖即可。

内联语法

| 语法 | 效果 | | ---------------------------------------- | ---------------------------------------- | | ==渐变背景文字== | 渐变背景强调 | | ::柔光重点文字:: | 柔光蓝紫色文字 | | !!胶囊文字!! | 超圆角胶囊背景 | | ^^加重强调^^ | 靛青加重文字 | | __下划线__ | 下划线 | | ~~删除线~~ | 删除线 | | 加粗 / 斜体 / 加粗斜体 | 基础强调 | | ` 行内代码 | 行内代码 | | $公式$ | 行内数学公式(MathJax) | | / / | 上标 / 下标 / HTML 下划线 | | | 彩色标签徽章 | | !alt[100% 120px] | 图片(方括号内为尺寸后缀) | | text | 脚注引用(带引号标题的链接自动转为脚注) |

块级组件

扩展组件通过 标签使用,全部组件均支持属性面板可视化编辑。

| 标签 | 组件 | 说明 | | ---------------- | --------------------- | ------------------------------------------------- | | <code> | 标题卡片 | 两种样式(DA01 / DA02) | | </code><p-title><code> | 段落标题 | 支持 </code>level<code> 指定大纲层级(1–4) | | </code><reading-path><code> | 阅读路线导航 | 根据文内标题自动生成 | | </code><statement><code> | 居中强调语 | - | | </code><lead><code> | 引导文字段 | - | | </code><breaking><code> | 突发/重大更新卡片 | - | | </code><compare><code> | Before/After 对比布局 | 两种样式 | | </code><cta><code> | 行动召唤卡片 | - | | </code><steps><code> | 横向步骤流 | 两种样式 | | </code><timeline><code> | 时间线组件 | - | | </code><engage><code> | 互动引导组件 | 两种样式 | | </code><case-flow><code> | 实践案例流程 | - | | </code><badges><code> | 彩色标签徽章 | - | | </code><table><code> | 表格扩展 | 支持 </code>{col:N}<code> / </code>{row:N}<code> / </code>{cover}<code> 单元格合并 | | </code><img><code> | 单图组件 | 宽高、圆角、裁切、容器对齐、偏移、阴影 | | </code><slider><code> | 图片幻灯片轮播 | 4 种轮播模式(循环 / 来回 / 滚回 / 淡入淡出) | | </code><chart><code> | 图表组件 | - | | </code><mermaid><code> | Mermaid 图表 | 流程图 / 时序图 / 甘特图 | | </code><row><code> | 横向布局 | - | | </code><column><code> | 纵向布局 | - | | </code><container><code> | 通用容器 | 支持嵌套 | | </code><stack><code> | 层叠舞台容器 | 配合 </code><positioned><code> 做绝对定位 | | </code><positioned><code> | 定位层 | 仅可在 </code><stack><code> 内使用 | | </code><text><code> | 文本样式 | 行内文本样式容器 | | </code><html><code> | HTML | 直通 HTML 片段 | | </code> ``<code> </code> | 代码块 | 语言头显示代码图标与语言名 | | <code>> [TIP]</code> | 提示框 | 支持 TIP / NOTE / WARNING / CAUTION / IMPORTANT |</p> <h2>🚀 快速开始</h2> <h3>环境要求</h3> <ul><li>Node.js >= 24</li> <li>pnpm</li> <li>Rust(仅桌面客户端开发需要)</li></ul> <h3>安装与运行</h3> <pre><code class="bash"># 克隆项目(含子模块) git clone --recursive https://github.com/RobocopMao/r-markdown.git cd r-markdown <h1>若已克隆但未带子模块,初始化子模块</h1> pnpm sm:update # 或 bash update-submodules.sh <h1>安装依赖</h1> pnpm install <h1>启动开发服务器</h1> pnpm dev</code></pre> <p>浏览器打开终端输出的地址即可使用。</p> <blockquote><code>pnpm dev</code> / <code>build</code> / <code>tauri:dev</code> / <code>tauri:build</code> 会先自动执行 <code>pnpm clean</code> 清理 <code>src/**.js</code> 编译产物。</blockquote> <blockquote>子模块缺失时 <code>pnpm build</code> 会自动把 <code>src/extension-stubs/</code> 复制到 <code>src/extension/</code> 作为空桩,保证编译通过。</blockquote> <h3>构建生产版本</h3> <pre><code class="bash">pnpm build</code></pre> <p>构建产物输出到 <code>dist/</code> 目录。</p> <h3>预览构建结果</h3> <pre><code class="bash">pnpm preview</code></pre> <h3>代码检查</h3> <pre><code class="bash">pnpm check # ESLint + Prettier 检查 pnpm lint # ESLint 自动修复 pnpm format # Prettier 格式化</code></pre> <h3>桌面客户端开发</h3> <pre><code class="bash"># 启动 Tauri 开发模式(热更新) pnpm tauri:dev <h1>构建桌面客户端</h1> pnpm tauri:build</code></pre> <p>构建产物:</p> <ul><li>macOS: <code>src-tauri/target/release/bundle/dmg/R-Markdown_*.dmg</code></li> <li>Windows: <code>src-tauri/target/release/bundle/msi/R-Markdown_*.msi</code></li></ul> <h3>桌面客户端安装</h3> <p>从 <a href="https://github.com/RobocopMao/r-markdown/releases" target="_blank" rel="noopener">GitHub Releases</a> 下载最新版本。</p> <p>macOS 首次打开时若提示"已损坏",执行以下命令放行:</p> <pre><code class="bash">sudo xattr -rd com.apple.quarantine /Applications/R-Markdown.app</code></pre> <h2>📦 技术栈</h2> <ul><li><strong>Vue 3</strong> (Composition API + <code><script setup></code>)</li> <li><strong>TypeScript</strong></li> <li><strong>Vite</strong> — 构建工具</li> <li><strong>Vue Router</strong> — 路由管理(hash 模式)</li> <li><strong>CodeMirror 6</strong> — Markdown 编辑器内核</li> <li><strong>Tailwind CSS 4</strong> — 样式系统</li> <li><strong>IndexedDB</strong> — 图片 / 草稿 / 素材 / 缓存本地存储</li> <li><strong>MathJax</strong> — 数学公式渲染</li> <li><strong>Mermaid</strong> — 流程图 / 时序图 / 甘特图</li> <li><strong>highlight.js</strong> — 代码块语法高亮(转内联样式,可直接粘贴公众号)</li> <li><strong>html-to-image</strong> — 图片导出</li> <li><strong>JSZip</strong> — 小红书图批量打包(桌面端)</li> <li><strong>mammoth</strong> — <code>.docx</code> 导入</li> <li><strong>PixiJS</strong> — 首页粒子特效</li> <li><strong>awesome-design-md</strong> — 公众号排版引擎</li> <li><strong>Tauri 2</strong> — 桌面客户端框架(macOS + Windows,Rust 后端调用微信 API)</li></ul> <h2>📁 项目结构</h2> <pre><code class="">r-markdown/ ├── src/ │ ├── components/ # 公用 UI 组件 │ │ ├── BaseDialog.vue # 通用弹窗 │ │ ├── BaseDrawer.vue # 通用抽屉 │ │ ├── BaseTooltip.vue # 通用 tooltip │ │ ├── ConfirmDialog.vue # 确认弹窗 │ │ ├── DarkModeToggle.vue # 暗色模式切换 │ │ ├── NavCapsule.vue # 顶部导航胶囊 │ │ ├── PromptDialog.vue # 提示词弹窗 │ │ ├── SiteFooter.vue # 页脚 │ │ ├── SiteLogo.vue # 站点 Logo │ │ ├── Toast.vue # 轻提示 │ │ └── mobile/ # 移动端专用组件 │ ├── composables/ # 全局组合式函数 │ │ ├── useTheme.ts # 主题管理 │ │ ├── useDarkMode.ts # 暗色模式 │ │ ├── useAutoUpdater.ts # Tauri 自动更新 │ │ ├── useMermaid.ts # Mermaid 渲染 │ │ ├── useEditorSettings.ts # 编辑器全局设置 │ │ ├── useParagraphSettings.ts# 段落格式设置 │ │ ├── useDropdownGroup.ts # 下拉菜单组 │ │ └── useSetting.ts # 通用设置读写 │ ├── config/ # 配置层 │ │ ├── defaults.ts # 全部设置项与默认值(新增设置只改这里) │ │ └── settings.ts # 设置读写(敏感项 AES-GCM-256 加密) │ ├── data/ # 静态数据 │ │ ├── demoContent.ts # 示例内容 │ │ └── ai/ # AI 能力用的技能/规范文本(与对应 skill 同源,手动同步) │ │ ├── r-markdown-formatter/ # AI 排版用的组件规范(format-spec.md) │ │ └── khazix/ # AI 写作用的内置技能(SKILL.md + references/) │ ├── extension/ # 排版组件库(git 子模块,闭源) │ ├── extension-stubs/ # 排版组件空桩(fallback) │ ├── router/ # 路由 │ ├── services/ # 业务服务层 │ │ ├── DraftStorage.ts # IndexedDB 草稿存储 │ │ ├── GitHubArticleCache.ts # GitHub 文章本地缓存 │ │ ├── GitHubTreeService.ts # GitHub 仓库文章树 CRUD │ │ ├── LocalTreeService.ts # 本地磁盘文章树 CRUD │ │ ├── localArticlePath.ts # 本地文章目录路径解析 │ │ ├── localArticleStorage.ts # 本地文章目录迁移 / 重装加载 │ │ ├── localImageDisk.ts # 本地磁盘图片存储 │ │ ├── articleWorkspace.ts # 文章工作区(多仓库 / 多分支 / 多目录) │ │ ├── encryption.ts # AES-GCM-256 加解密 │ │ ├── githubUploader.ts # GitHub 图床 │ │ ├── letaUploader.ts # 乐塔图床 │ │ ├── materialLibrary.ts # 官方素材库 │ │ ├── materialStorage.ts # 我的素材存储 │ │ ├── materialPublish.ts # 素材发布 │ │ ├── coverCache.ts # 微信封面缓存 │ │ ├── configPersistence.ts # 配置持久化 │ │ ├── aiFormat.ts # AI 排版 / 通用 chat.completions 调用(chatCompletion) │ │ ├── aiWrite.ts # AI 写作:在「AI 文本」共用配置上叠加写作专属采样参数 │ │ ├── aiWriteSessionStore.ts # AI 写作会话持久化(IndexedDB,单条记录) │ │ ├── imageGen.ts # AI 配图生图调用 │ │ ├── imageProviders.ts # AI 配图厂商注册表 │ │ ├── apiBase.ts # 基址归一化 / 错误中文映射 / 超时中止(AI 能力共用) │ │ ├── startupCheck.ts # 启动配置恢复检查 │ │ └── wechatPublisher.ts # 微信公众号发布 │ ├── styles/ # 全局样式 │ ├── utils/ # 工具函数 │ │ ├── articleNaming.ts # 文章文件命名(文章名净化 / 短哈希 / 唯一基名) │ │ ├── markdownParser.ts # Markdown → HTML 解析 │ │ ├── inlineFormat.ts # 行内格式化 │ │ ├── components.ts # 组件解析(callout / timeline 等) │ │ ├── headingLevels.ts # 标题层级唯一权威定义 │ │ ├── outline.ts # 文档大纲提取 │ │ ├── findReplace.ts # 查找替换纯逻辑 │ │ ├── charCount.ts # 字数统计与阅读时长 │ │ ├── commandPalette.ts # 命令面板检索逻辑 │ │ ├── bannedWords.ts # 违禁词词库与扫描 │ │ ├── paginate.ts # PDF 导出 DOM 测量分页 │ │ ├── platform.ts # 平台判断与快捷键文案 │ │ ├── colorUtils.ts # 颜色处理 │ │ ├── extractTitle.ts # 标题提取 │ │ ├── helpers.ts # 通用辅助(esc/leaf/hexToRgb/getErrorMessage 等) │ │ ├── imageDB.ts # IndexedDB 图片存储 │ │ ├── mathRenderer.ts # MathJax 公式渲染 │ │ ├── aiWritePrompt.ts # AI 写作:prompt 组装与输出分类/清洗(纯逻辑) │ │ ├── aiWriteSession.ts # AI 写作:会话模型、有效期与历史裁剪(纯逻辑) │ │ └── xhsCards.ts # 小红书卡片 │ ├── views/ # 页面视图 │ │ ├── home/HomePage.vue # 首页 │ │ ├── editor/ │ │ │ ├── EditorPage.vue # 编辑器页 │ │ │ ├── components/ # 编辑器专用组件(见下表) │ │ │ └── composables/ # 编辑器专用组合式函数 │ │ ├── extension/ExtensionPage.vue # 组件展示页 │ │ ├── help/ # 帮助文档(公开降级版) │ │ ├── material/ # 素材库(公开降级版) │ │ └── 404/NotFound.vue # 404 页 │ ├── views-private/ # 私有视图(git 子模块,闭源):私有首页 / 素材库 / 帮助 │ ├── App.vue # 根组件 │ └── main.ts # 入口 ├── src-tauri/ # Tauri 桌面客户端(git 子模块,Rust) ├── worker/ # Cloudflare Worker:GitHub API 代理(素材库发布鉴权) ├── functions/ # Cloudflare Pages Function:/api/leta 乐塔图床代理 ├── scripts/ │ ├── clean-artifacts.mjs # 清理 src/**.js 编译产物 │ └── ensure-extension-stubs.mjs # 子模块缺失时复制空桩 ├── .github/workflows/ │ ├── deploy.yml # GitHub Pages 部署(手动触发) │ └── build-desktop.yml # 桌面端 CI/CD 构建(tag 触发) ├── package.json ├── vite.config.ts └── tsconfig.json</code></pre> <h3>编辑器组件一览(<code>src/views/editor/components/</code>)</h3> <p>| 组件 | 用途 | | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------- | | <code>Editor.vue</code> | CodeMirror 编辑器封装 | | <code>Preview.vue</code> | 预览区渲染与复制 | | <code>EditorSidebar.vue</code> | 左侧竖排功能入口(文章 / 草稿 / 导入 / 组件 / 图库 / 素材 / 检测 / 示例 / 帮助) | | <code>TreeSidebar.vue</code> <code>TreeNode.vue</code> | 文章树侧栏与节点 | | <code>OutlinePanel.vue</code> | 文档大纲面板 | | <code>FindReplacePanel.vue</code> | 查找替换面板 | | <code>CommandPalette.vue</code> | 命令面板(⌘K) | | <code>Minimap.vue</code> | 预览区缩略图(可开关) | | <code>BannedWordsDialog.vue</code> | 违禁词检测弹窗 | | <code>SettingsDialog.vue</code> | 设置弹窗(基础 / 图片 / 文章存储 / 公众号 / 关于) | | <code>ThemePicker.vue</code> | 主题色选择 | | <code>ComponentPickerDialog.vue</code> <code>TagPropsForm.vue</code> | 组件插入与属性编辑 | | <code>ImageCacheDialog.vue</code> | 图库弹窗 | | <code>DraftListDialog.vue</code> <code>SaveDraftDialog.vue</code> | 草稿列表与保存草稿 | | <code>PushToCloudDialog.vue</code> <code>PushToCloudTree.vue</code> | 推送到云端文章树 | | <code>PublishToWechatDialog.vue</code> | 发布到公众号 | | <code>MaterialLibraryPanel.vue</code> <code>MaterialCard.vue</code> <code>SaveMaterialDialog.vue</code> | 素材面板与卡片 | | <code>XhsExporter.vue</code> | 小红书图导出 | | <code>FinalizeDialog.vue</code> | 定稿(本地导出 Markdown) | | <code>Dropdown.vue</code> | 通用下拉 | | <code>mobile/MobileActionsMenu.vue</code> | 移动端操作菜单 |</p> <h2>🌐 在线体验</h2> <ul><li><strong>Cloudflare Pages(主站,含乐塔图床代理)</strong>:<a href="https://r-markdown.pages.dev/" target="_blank" rel="noopener">r-markdown.pages.dev</a></li> <li><strong>GitHub Pages(<code>deploy.yml</code> 手动触发部署)</strong>:<a href="https://RobocopMao.github.io/r-markdown/" target="_blank" rel="noopener">RobocopMao.github.io/r-markdown</a></li></ul> <h2>📄 License</h2> <p>MIT</p> </article> <!-- Article Completion Section --> <div class="reader-footer mt-5 pt-5"> <div class="reader-completion-box rounded-3 p-4 text-center"> <div class="reader-completion-icon mb-3"> <i class="fas fa-check-circle"></i> </div> <p class="reader-completion-text fw-semibold mb-1" style="font-size: 1rem;">You finished the article!</p> <p class="reader-completion-subtext small mb-4">Want to go deeper or explore more stories?</p> <div class="d-flex justify-content-center gap-3 flex-wrap"> <a href="https://github.com/RobocopMao/r-markdown" target="_blank" rel="noopener noreferrer" class="btn btn-primary-custom btn-sm rounded-pill px-4"> <i class="fas fa-external-link-alt me-2"></i>Full Article on GitHub Trending </a> <a href="https://neshdevtech.com/news" class="reader-back-btn btn btn-sm rounded-pill px-4"> <i class="fas fa-newspaper me-2"></i>More Tech News </a> </div> </div> </div> <!-- Internal Linking CTA --> </div> </div> </div> <!-- Sidebar (desktop only) --> <div class="col-lg-4 col-xl-3 d-none d-lg-block"> <div class="sticky-top" style="top: 80px;"> <!-- Mini Controller Widget --> <div class="card border-0 shadow-sm mb-4 bg-surface"> <div class="card-body p-4"> <h6 class="fw-bold mb-3"><i class="fas fa-glasses me-2 text-primary-custom"></i>Reading Mode</h6> <p class="text-secondary-custom small mb-4">Adjust typography and theme to your preference.</p> <div class="mb-3"> <label class="text-muted-custom small d-block mb-2">Typography</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-serif">Serif</button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-font-sans">Sans</button> </div> </div> <div class="mb-4"> <label class="text-muted-custom small d-block mb-2">Text Scale</label> <div class="d-flex gap-2"> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-dec"><i class="fas fa-minus small"></i></button> <button class="btn btn-light btn-sm flex-grow-1 border-custom" id="sidebar-size-inc"><i class="fas fa-plus small"></i></button> </div> </div> <div class="border-top border-light-custom pt-3"> <a href="https://github.com/RobocopMao/r-markdown" target="_blank" rel="noopener noreferrer" class="btn btn-outline-primary btn-sm w-100 rounded-pill"> View Original <i class="fas fa-external-link-alt ms-1"></i> </a> </div> </div> </div> <!-- Sidebar Related News --> <div class="card border-0 shadow-sm bg-surface"> <div class="card-header bg-transparent border-0 pt-4 pb-0 px-4"> <h5 class="card-title fw-bold mb-0" style="font-size: 1.05rem;"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h5> </div> <div class="card-body p-0"> <div class="list-group list-group-flush rounded-0 bg-transparent"> <a href="https://neshdevtech.com/news/how-to-formalize-all-of-mathematics-Q4VIm" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> How to Formalize All of Mathematics </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/what-are-your-chances-of-surviving-resident-evil-based-on-your-tech-job-9Z2qb" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> What Are Your Chances of Surviving Resident Evil Based on Your Tech Job? </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/what-i-actually-learn-when-i-build-with-ai-xZsxU" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> What I Actually Learn When I Build With AI </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/natural-languages-have-design-trade-offs-too-georgian-is-dynamically-typed-english-is-statically-typed-sHeGa" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> Natural Languages Have Design Trade-offs Too: Georgian Is Dynamically Typed, English Is Statically Typed </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Dev.to</span> <span>3 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/i-know-why-mathematicians-weep-2CQVG" class="list-group-item list-group-item-action border-0 px-4 py-3 bg-transparent border-bottom border-light-custom hover-bg-light"> <h6 class="mb-1 fw-semibold text-truncate-2" style="font-size: 0.875rem; line-height: 1.4;"> I know why Mathematicians weep </h6> <div class="d-flex align-items-center justify-content-between text-muted-custom mt-1" style="font-size: 0.72rem;"> <span>Hacker News</span> <span>3 days ago</span> </div> </a> </div> </div> </div> </div> </div> </div> <!-- Mobile: Related News horizontal scroll strip --> <div class="d-block d-lg-none mt-4"> <h6 class="fw-bold mb-3 px-1"> <i class="fas fa-newspaper me-2 text-primary-custom"></i>Related Stories </h6> <div class="related-scroll-strip"> <a href="https://neshdevtech.com/news/how-to-formalize-all-of-mathematics-Q4VIm" class="related-scroll-card"> <p class="related-scroll-title">How to Formalize All of Mathematics</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/what-are-your-chances-of-surviving-resident-evil-based-on-your-tech-job-9Z2qb" class="related-scroll-card"> <p class="related-scroll-title">What Are Your Chances of Surviving Resident Evil Based on Your Tech Jo...</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/what-i-actually-learn-when-i-build-with-ai-xZsxU" class="related-scroll-card"> <p class="related-scroll-title">What I Actually Learn When I Build With AI</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>2 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/natural-languages-have-design-trade-offs-too-georgian-is-dynamically-typed-english-is-statically-typed-sHeGa" class="related-scroll-card"> <p class="related-scroll-title">Natural Languages Have Design Trade-offs Too: Georgian Is Dynamically...</p> <div class="related-scroll-meta"> <span>Dev.to</span> <span>3 days ago</span> </div> </a> <a href="https://neshdevtech.com/news/i-know-why-mathematicians-weep-2CQVG" class="related-scroll-card"> <p class="related-scroll-title">I know why Mathematicians weep</p> <div class="related-scroll-meta"> <span>Hacker News</span> <span>3 days ago</span> </div> </a> </div> </div> </div> </section> </main> <!-- Footer --> <!-- Footer --> <footer class="py-5 mt-5 position-relative overflow-hidden" style="background: var(--surface); border-top: 1px solid var(--border); box-shadow: var(--shadow-lg);"> <!-- Decorative subtle background gradients --> <div class="position-absolute" style="top: -200px; right: -200px; width: 400px; height: 400px; background: rgba(var(--primary-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="position-absolute" style="bottom: -200px; left: -200px; width: 400px; height: 400px; background: rgba(var(--accent-rgb), 0.03); filter: blur(100px); border-radius: 50%; pointer-events: none;"></div> <div class="container position-relative z-3"> <div class="row g-4"> <!-- Column 1: Brand & About --> <div class="col-lg-4 col-md-6"> <div class="d-flex align-items-center mb-3"> <span class="brand-mark me-2" style="width: 42px; height: 42px;" aria-hidden="true"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg> </span> <span class="fs-4 fw-extrabold text-primary-custom" style="letter-spacing: -0.05em; font-family: 'JetBrains Mono', monospace;">NeshDevTech</span> </div> <p class="text-secondary-custom mb-4 pe-lg-4" style="font-size: 0.95rem; line-height: 1.6;">NeshDevTech is a professional technology firm passionate about creating innovative, high-performance web and mobile solutions.</p> <div class="d-flex gap-2"> </div> </div> <!-- Column 2: Navigation Links --> <div class="col-lg-2 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Quick Navigation</h6> <ul class="list-unstyled mb-0"> <li class="mb-2"><a href="https://neshdevtech.com" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Home</a></li> <li class="mb-2"><a href="https://neshdevtech.com/projects" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Projects</a></li> <li class="mb-2"><a href="https://neshdevtech.com/blog" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Tech Blog</a></li> <li class="mb-2"><a href="https://neshdevtech.com/pricing" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Get Quote</a></li> <li class="mb-2"><a href="https://neshdevtech.com/contact" class="footer-link text-secondary-custom text-decoration-none d-inline-block">Contact Me</a></li> </ul> </div> <!-- Column 3: Tech Stack & Services --> <div class="col-lg-3 col-md-6 col-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Core Services</h6> <ul class="list-unstyled mb-0"> <li class="text-secondary-custom mb-2 small"><i class="bi bi-rocket-takeoff text-primary-custom me-2"></i>Custom Laravel Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-laptop text-primary-custom me-2"></i>SPA (React & Vue.js)</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-phone-vibrate text-primary-custom me-2"></i>Flutter Mobile Apps</li> <li class="text-secondary-custom mb-2 small"><i class="bi bi-search text-primary-custom me-2"></i>SEO & Performance Opt</li> </ul> </div> <!-- Column 4: Contact & Availability --> <div class="col-lg-3 col-md-6"> <h6 class="text-primary-custom fw-bold text-uppercase mb-3" style="font-size: 0.85rem; letter-spacing: 0.05em;">Get in Touch</h6> <ul class="list-unstyled mb-3"> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-geo-alt text-primary-custom me-2 fs-6"></i>Nairobi, Kenya </li> <li class="text-secondary-custom mb-2 small d-flex align-items-center"> <i class="bi bi-envelope-at text-primary-custom me-2 fs-6"></i> <a href="mailto:info@neshdevtech.com" class="text-secondary-custom text-decoration-none footer-link">info@neshdevtech.com</a> </li> </ul> <div class="bg-primary-custom-light border border-primary-custom border-opacity-10 p-3 rounded-3" style="border-radius: 12px !important;"> <small class="d-block fw-semibold text-primary-custom mb-1"><i class="bi bi-patch-check-fill me-1"></i>Available for Projects</small> <p class="mb-0 text-secondary-custom small" style="font-size: 0.8rem; line-height: 1.4;">Need a high-performing system? Let's discuss your project.</p> </div> </div> </div> <hr style="border-color: var(--border); opacity: 0.8; margin: 2.5rem 0 2rem;"> <div class="row align-items-center"> <div class="col-md-6 text-center text-md-start mb-2 mb-md-0"> <p class="mb-0 text-secondary-custom small">© 2026 NeshDevTech. All rights reserved.</p> </div> <div class="col-md-6 text-center text-md-end"> <p class="mb-0 text-secondary-custom small">Built with <i class="bi bi-heart-fill text-danger"></i></p> </div> </div> </div> </footer> <!-- Bootstrap JS --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <!-- Custom JavaScript --> <script> // Theme Management System class ThemeManager { constructor() { this.themeToggle = document.getElementById('themeToggle'); this.themeIcon = document.getElementById('theme-icon'); this.mobileThemeToggle = document.getElementById('mobileThemeToggle'); this.mobileThemeIcon = document.getElementById('mobile-theme-icon'); this.init(); } init() { // Get current theme (already applied in head) const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; // Update icons to match current theme this.updateIcon(currentTheme); // Listen for desktop theme toggle clicks if (this.themeToggle) { this.themeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for mobile theme toggle clicks if (this.mobileThemeToggle) { this.mobileThemeToggle.addEventListener('click', (e) => { e.preventDefault(); this.toggleTheme(); }); } // Listen for system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { if (!localStorage.getItem('theme')) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); document.documentElement.setAttribute('data-bs-theme', theme); if (theme === 'dark') { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } localStorage.setItem('theme', theme); this.updateThemeColor(theme); this.updateIcon(theme); } updateThemeColor(theme) { document.querySelectorAll('meta[name="theme-color"]').forEach((meta) => { meta.setAttribute('content', theme === 'dark' ? '#1c2028' : '#e4e8f0'); }); } toggleTheme() { const currentTheme = document.documentElement.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; this.setTheme(newTheme); // Close mobile menu if open const navbarCollapse = document.querySelector('.navbar-collapse'); if (navbarCollapse && navbarCollapse.classList.contains('show') && window.innerWidth < 992) { setTimeout(() => { const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); }, 150); } } updateIcon(theme) { const iconClass = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; // Update desktop icon if (this.themeIcon) { this.themeIcon.className = iconClass; } // Update mobile icon if (this.mobileThemeIcon) { this.mobileThemeIcon.className = iconClass; } } } // Initialize theme manager when DOM is loaded document.addEventListener('DOMContentLoaded', () => { new ThemeManager(); // Initialize navbar live clock (function initNavClock(){ const clockEls = Array.from(document.querySelectorAll('[data-clock]')); if (!clockEls.length) return; const locale = navigator.language || 'en-US'; // Detect 12/24h by locale: use resolvedOptions hourCycle when available const dtf = new Intl.DateTimeFormat(locale, { hour: 'numeric' }); const uses12h = (dtf.resolvedOptions().hour12 === true); const fmt = new Intl.DateTimeFormat(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: uses12h }); const update = () => { const now = new Date(); const text = fmt.format(now); clockEls.forEach(el => { el.textContent = text; }); }; update(); setInterval(update, 1000); })(); }); // Smooth scrolling for anchor links document.querySelectorAll('a[href^="#"]').forEach(anchor => { anchor.addEventListener('click', function (e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } }); }); // Navbar background on scroll window.addEventListener('scroll', () => { const navbar = document.querySelector('.navbar'); if (window.scrollY > 50) { navbar.style.backdropFilter = 'blur(20px)'; navbar.style.backgroundColor = 'rgba(var(--background-rgb), 0.8)'; } else { navbar.style.backdropFilter = 'blur(10px)'; navbar.style.backgroundColor = 'var(--background)'; } }); // Add fade-in animation to elements when they come into view const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('animate-fade-in-up'); } }); }, observerOptions); // Observe elements with animation class document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.animate-on-scroll').forEach(el => { observer.observe(el); }); }); // Enhanced mobile menu functionality document.addEventListener('DOMContentLoaded', function() { const navbarToggler = document.querySelector('.navbar-toggler'); const navbarCollapse = document.querySelector('.navbar-collapse'); const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); const socialLinks = document.querySelectorAll('.navbar-nav .d-flex a'); let isToggling = false; // Prevent rapid clicking // Enhanced menu close function function closeMenu() { if (navbarCollapse && navbarCollapse.classList.contains('show')) { // Use Bootstrap's collapse API for smooth transition const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.hide(); } // Reset hamburger animation and aria-expanded if (navbarToggler) { navbarToggler.classList.remove('active'); navbarToggler.setAttribute('aria-expanded', 'false'); } } // Enhanced hamburger toggle functionality if (navbarToggler) { navbarToggler.addEventListener('click', function(e) { e.preventDefault(); // Prevent rapid clicking if (isToggling) return; isToggling = true; const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen) { closeMenu(); } else { // Open menu const bsCollapse = new bootstrap.Collapse(navbarCollapse, { toggle: false }); bsCollapse.show(); navbarToggler.setAttribute('aria-expanded', 'true'); } // Reset toggle flag after animation setTimeout(() => { isToggling = false; }, 350); }); } // Close menu when clicking outside document.addEventListener('click', function(event) { const isClickInsideNav = navbarCollapse.contains(event.target); const isToggler = navbarToggler.contains(event.target); const isMenuOpen = navbarCollapse.classList.contains('show'); if (!isClickInsideNav && !isToggler && isMenuOpen && window.innerWidth < 992) { closeMenu(); } }); // Close menu when clicking on navigation links (mobile) navLinks.forEach(function(link) { link.addEventListener('click', function(e) { const isMenuOpen = navbarCollapse.classList.contains('show'); if (isMenuOpen && window.innerWidth < 992 && !this.classList.contains('dropdown-toggle')) { // Add a small delay to let the link navigation start setTimeout(() => { closeMenu(); }, 100); } }); }); // Close menu when clicking on social links (mobile) socialLinks.forEach(function(link) { link.addEventListener('click', function() { if (window.innerWidth < 992) { setTimeout(() => { closeMenu(); }, 100); } }); }); // Escape key closes menu document.addEventListener('keydown', function(event) { if (event.key === 'Escape' && window.innerWidth < 992) { closeMenu(); } }); // Handle window resize to close menu when switching to desktop window.addEventListener('resize', function() { if (window.innerWidth >= 992) { closeMenu(); } }); // Theme toggle functionality is handled by ThemeManager class // No separate event listener needed here // Prevent menu items from being too small on touch devices if ('ontouchstart' in window) { navLinks.forEach(function(link) { link.style.minHeight = '44px'; link.style.display = 'flex'; link.style.alignItems = 'center'; }); } // Add smooth scrolling for anchor links within pages navLinks.forEach(function(link) { const href = link.getAttribute('href'); if (href && href.startsWith('#')) { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(href); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); } closeMenu(); }); } }); }); </script> <script> // Sticky navbar scroll enhancement (desktop only) (function () { const navbar = document.querySelector('.navbar'); if (!navbar) return; let ticking = false; const SCROLL_THRESHOLD = 20; // px function updateNavbar() { if (window.innerWidth >= 992) { if (window.scrollY > SCROLL_THRESHOLD) { navbar.classList.add('scrolled'); } else { navbar.classList.remove('scrolled'); } } else { // Always remove on mobile so it doesn't affect mobile look navbar.classList.remove('scrolled'); } ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { requestAnimationFrame(updateNavbar); ticking = true; } }, { passive: true }); window.addEventListener('resize', updateNavbar, { passive: true }); // Run once on page load in case page is already scrolled (e.g. back-navigation) updateNavbar(); })(); </script> <script> // Lightweight toast badge notifications (function(){ function ensureContainer(){ let c = document.getElementById('toast-badge-container'); if(!c){ c = document.createElement('div'); c.id = 'toast-badge-container'; c.setAttribute('aria-live','polite'); c.setAttribute('aria-atomic','true'); document.body.appendChild(c); } return c; } function iconFor(type){ switch(type){ case 'success': return '<i class="bi bi-check-circle"></i>'; case 'warning': return '<i class="bi bi-exclamation-triangle"></i>'; case 'error': return '<i class="bi bi-x-circle"></i>'; default: return '<i class="bi bi-info-circle"></i>'; } } window.notifyBadge = function(message, opts={}){ const { type = 'success', timeout = 2500 } = opts; const container = ensureContainer(); const el = document.createElement('div'); el.className = 'toast-badge ' + type; el.setAttribute('role','status'); el.innerHTML = '<span class="icon">'+iconFor(type)+'</span><span>'+message+'</span>'; container.appendChild(el); requestAnimationFrame(()=>{ el.classList.add('show'); }); const remove = () => { el.classList.remove('show'); setTimeout(()=> el.remove(), 200); }; setTimeout(remove, timeout); return { remove }; }; // Global copy helper used by inline buttons window.copyToClipboard = function(url){ try { navigator.clipboard.writeText(url).then(()=>{ // Update the clicked button, if available let btn = document.activeElement && document.activeElement.tagName === 'BUTTON' ? document.activeElement : (window.event && window.event.target && window.event.target.closest ? window.event.target.closest('button') : null); if (btn){ const original = btn.innerHTML; btn.innerHTML = '<i class="fas fa-check me-1"></i> Copied!'; btn.classList.remove('btn-outline-secondary'); btn.classList.add('btn-success'); setTimeout(()=>{ btn.innerHTML = original; btn.classList.remove('btn-success'); btn.classList.add('btn-outline-secondary'); }, 2000); } notifyBadge('Link copied to clipboard', { type: 'success' }); }).catch((err)=>{ console.error('Clipboard error:', err); notifyBadge('Could not copy link', { type: 'error' }); }); } catch (e) { console.error(e); notifyBadge('Copy not supported', { type: 'error' }); } }; })(); </script> <a href="https://wa.me/254768033943?text=Hi%2C+I+visited+your+website+and+would+like+to+inquire+about+your+services." class="whatsapp-btn-float" target="_blank" rel="noopener noreferrer" aria-label="Chat on WhatsApp"> <i class="fab fa-whatsapp"></i> <span class="whatsapp-tooltip">Chat with me</span> </a> <style> .whatsapp-btn-float { position: fixed; bottom: 24px; right: 24px; z-index: 1050; display: flex; align-items: center; justify-content: center; background-color: #25d366; color: #fff !important; width: 60px; height: 60px; border-radius: 50%; box-shadow: 0 4px 16px rgba(37, 211, 102, 0.45); text-decoration: none; transition: all 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); } .whatsapp-btn-float i { font-size: 32px; transition: transform 0.3s ease; } .whatsapp-btn-float:hover { background-color: #128c7e; transform: scale(1.1); box-shadow: 0 6px 20px rgba(18, 140, 126, 0.6); } .whatsapp-btn-float:hover i { transform: rotate(10deg); } /* Pulsing ring animation */ .whatsapp-btn-float::before { content: ''; position: absolute; width: 100%; height: 100%; border-radius: 50%; background-color: #25d366; opacity: 0.7; z-index: -1; animation: whatsapp-pulse 2s infinite; } @keyframes whatsapp-pulse { 0% { transform: scale(1); opacity: 0.7; } 100% { transform: scale(1.6); opacity: 0; } } /* Tooltip style utilizing site-wide theme colors */ .whatsapp-btn-float .whatsapp-tooltip { position: absolute; right: 76px; background-color: var(--surface); color: var(--text-primary); border: 1px solid var(--border); padding: 8px 14px; border-radius: 20px; font-size: 14px; font-weight: 600; white-space: nowrap; opacity: 0; visibility: hidden; transform: translateX(10px); transition: all 0.3s ease; box-shadow: var(--shadow); pointer-events: none; } .whatsapp-btn-float:hover .whatsapp-tooltip { opacity: 1; visibility: visible; transform: translateX(0); } /* Responsive Design */ @media (max-width: 768px) { .whatsapp-btn-float { bottom: 16px; right: 16px; width: 50px; height: 50px; } .whatsapp-btn-float i { font-size: 26px; } .whatsapp-btn-float .whatsapp-tooltip { display: none; /* Hide tooltip on small screens to prevent overlap issues */ } } </style> <!-- Mobile More Menu (Offcanvas Bottom Sheet) - Auth actions only --> <style> /* ══════════════════════════════════════════════════════════════ NEUMORPHISM REVAMP (front-end only) Soft-extruded UI: same-tone surfaces with dual light/dark shadows. This block sits at the end of the body so it layers over every page's own styles without touching page markup. ══════════════════════════════════════════════════════════════ */ /* ── Buttons: extruded, press-in on click ─────────────────────── */ .btn { box-shadow: var(--shadow) !important; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1) !important; } .btn:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .btn:active:not(.btn-link), .btn.show { transform: translateY(1px) scale(0.99) !important; box-shadow: var(--shadow-inset) !important; } .btn-primary-custom, .btn-primary, .btn-success { background: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow) !important; } .btn-primary-custom:hover, .btn-primary:hover, .btn-success:hover { background: var(--primary-dark) !important; border-color: var(--primary-dark) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-primary-custom, .btn-outline-primary, .btn-outline-success, .btn-outline-secondary, .btn-outline-danger, .btn-outline-dark, .btn-light { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow) !important; } .btn-outline-primary-custom:hover, .btn-outline-primary:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-success:hover { background-color: var(--primary-color) !important; border-color: var(--primary-color) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-secondary:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-danger:hover { background-color: #ef4444 !important; border-color: #ef4444 !important; color: #fff !important; box-shadow: var(--shadow-md) !important; } .btn-outline-dark:hover, .btn-light:hover { background-color: var(--text-secondary) !important; border-color: var(--text-secondary) !important; color: var(--background) !important; box-shadow: var(--shadow-md) !important; } .btn-dark { background-color: var(--text-primary) !important; border-color: var(--text-primary) !important; color: var(--background) !important; box-shadow: var(--shadow) !important; } /* ── Forms: pressed-in surfaces ────────────────────────────────── */ .form-control, .form-select { background-color: var(--surface) !important; border: 1px solid transparent !important; color: var(--text-primary) !important; border-radius: 14px !important; box-shadow: var(--shadow-inset) !important; transition: box-shadow 0.3s ease, background-color 0.25s ease !important; } .form-control:focus, .form-select:focus { background-color: var(--surface) !important; border-color: transparent !important; color: var(--text-primary) !important; box-shadow: var(--shadow-inset), 0 0 0 4px rgba(var(--primary-rgb), 0.15) !important; } .form-control::placeholder { color: var(--text-muted) !important; } .input-group-text { background-color: var(--surface-alt) !important; border: 1px solid transparent !important; color: var(--text-secondary) !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Navbar: floating neumorphic pill (tablet & desktop) ──────── */ .navbar { background-color: rgba(var(--background-rgb), 0.9) !important; border-bottom: 1px solid transparent !important; backdrop-filter: blur(16px) !important; -webkit-backdrop-filter: blur(16px) !important; box-shadow: var(--shadow-md) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-md) !important; } @media (min-width: 768px) { .navbar { top: 14px !important; left: 18px !important; right: 18px !important; width: auto !important; border-radius: 20px !important; padding: 0.6rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar { box-shadow: var(--shadow-lg) !important; } .navbar.scrolled { background-color: rgba(var(--background-rgb), 0.95) !important; border-bottom-color: transparent !important; padding: 0.5rem 0 !important; box-shadow: var(--shadow-lg) !important; } [data-theme="dark"] .navbar.scrolled { background-color: rgba(28, 32, 40, 0.95) !important; box-shadow: var(--shadow-lg) !important; } body { padding-top: 96px !important; } .reading-progress-container { top: 88px !important; } } /* ── Tablet refinements (768px - 991.98px) ────────────────────── */ @media (min-width: 768px) and (max-width: 991.98px) { /* Hamburger dropdown panel blends with the floating pill */ .navbar-collapse { border-radius: 20px !important; border-color: transparent !important; } body { padding-bottom: 0 !important; } .whatsapp-btn-float { bottom: 24px !important; } /* Hero: tighten the inline 80px top padding that assumed a full-height bar, so tablets don't get excessive empty space */ .hero-section-bg { padding-top: 40px !important; } } /* ── Neumorphic brand icon mark ────────────────────────────────── */ .brand-mark { width: 40px; height: 40px; flex-shrink: 0; border-radius: 13px; background-color: var(--surface) !important; color: var(--primary-color) !important; display: inline-flex; align-items: center; justify-content: center; box-shadow: var(--shadow) !important; transition: box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), transform 0.3s cubic-bezier(0.16, 1, 0.3, 1); } .brand-mark svg { width: 21px; height: 21px; } a:hover .brand-mark { box-shadow: var(--shadow-inset-sm) !important; transform: translateY(1px); } .mobile-logo .brand-mark { width: 34px; height: 34px; border-radius: 11px; } .mobile-logo .brand-mark svg { width: 17px; height: 17px; } /* ── Uploaded brand logo image ─────────────────────────────────── */ .brand-img { height: 38px; width: auto; max-width: 160px; object-fit: contain; flex-shrink: 0; } .brand-img-sm { height: 28px; max-width: 120px; } .brand-img-footer { height: 44px; max-width: 180px; } /* ── Mobile bars: floating neumorphic pills (phones) ──────────── */ @media (max-width: 767.98px) { .mobile-top-bar { top: 8px !important; left: 12px !important; right: 12px !important; height: 52px !important; border-radius: 16px !important; border-bottom: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } .mobile-bottom-bar { left: 12px !important; right: 12px !important; bottom: 12px !important; height: 58px !important; border-radius: 18px !important; border-top: none !important; background-color: rgba(var(--surface-rgb), 0.92) !important; box-shadow: var(--shadow-md) !important; } body { padding-top: 74px !important; padding-bottom: 86px !important; } .whatsapp-btn-float { bottom: 88px !important; } .reading-progress-container { top: 70px !important; } } /* ── Nav pill widgets: pressed-in ──────────────────────────────── */ .theme-toggle, .mobile-theme-toggle, .nav-clock { background-color: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow-inset-sm) !important; } /* ── Icon chips, markers & circles: extruded ───────────────────── */ .icon-wrap, .icon-wrapper, .expertise-card-icon, .timeline-marker, .mobile-more-icon-wrapper, .social-circle, .value-icon, .skill-icon-box { box-shadow: var(--shadow) !important; border-color: transparent !important; } /* ── Panels: extruded ──────────────────────────────────────────── */ .accordion-item { border-color: transparent !important; box-shadow: var(--shadow) !important; } .timeline-content { background: var(--surface) !important; border-radius: 12px !important; padding: 1rem 1.25rem !important; box-shadow: var(--shadow) !important; } .feature-item { background: var(--surface) !important; border-color: transparent !important; box-shadow: var(--shadow) !important; } .feature-item:hover { transform: translateY(-2px) !important; box-shadow: var(--shadow-md) !important; } .progress { background-color: var(--surface-alt) !important; box-shadow: var(--shadow-inset-sm) !important; } /* Card hover: consistent soft neu shadow */ .card-hover:hover, .project-card:hover, .blog-card:hover, .post-card:hover, .news-card:hover, .testimonial-card:hover, .hover-lift:hover, .stats-item:hover { box-shadow: var(--shadow-lg) !important; } </style> <script> document.addEventListener('DOMContentLoaded', function() { const readerCard = document.getElementById('reader-card'); if (!readerCard) return; // Settings Key constants const THEME_KEY = 'reader_mode_theme'; const FONT_KEY = 'reader_mode_font'; const SIZE_KEY = 'reader_mode_size'; // Available Options const themes = ['light', 'sepia', 'dark', 'oled']; const fonts = ['serif', 'sans']; const sizes = ['xs', 'sm', 'md', 'lg', 'xl']; // Default Configs const siteTheme = document.documentElement.getAttribute('data-theme') || 'light'; const defaultTheme = siteTheme === 'dark' ? 'dark' : 'light'; let currentTheme = localStorage.getItem(THEME_KEY) || defaultTheme; let currentFont = localStorage.getItem(FONT_KEY) || 'serif'; let currentSize = localStorage.getItem(SIZE_KEY) || 'md'; // Apply initial config applySettings(); // Register Action Listeners (Toolbar) document.getElementById('btn-font-serif').addEventListener('click', () => setFont('serif')); document.getElementById('btn-font-sans').addEventListener('click', () => setFont('sans')); document.getElementById('btn-size-decrease').addEventListener('click', () => adjustSize(-1)); document.getElementById('btn-size-increase').addEventListener('click', () => adjustSize(1)); document.getElementById('theme-light').addEventListener('click', () => setTheme('light')); document.getElementById('theme-sepia').addEventListener('click', () => setTheme('sepia')); document.getElementById('theme-dark').addEventListener('click', () => setTheme('dark')); document.getElementById('theme-oled').addEventListener('click', () => setTheme('oled')); // Register Action Listeners (Sidebar widgets, if present) const sidebarSerif = document.getElementById('sidebar-font-serif'); const sidebarSans = document.getElementById('sidebar-font-sans'); const sidebarSizeDec = document.getElementById('sidebar-size-dec'); const sidebarSizeInc = document.getElementById('sidebar-size-inc'); if (sidebarSerif) sidebarSerif.addEventListener('click', () => setFont('serif')); if (sidebarSans) sidebarSans.addEventListener('click', () => setFont('sans')); if (sidebarSizeDec) sidebarSizeDec.addEventListener('click', () => adjustSize(-1)); if (sidebarSizeInc) sidebarSizeInc.addEventListener('click', () => adjustSize(1)); // Action Core Functions function setTheme(theme) { currentTheme = theme; localStorage.setItem(THEME_KEY, theme); applySettings(); } function setFont(font) { currentFont = font; localStorage.setItem(FONT_KEY, font); applySettings(); } function adjustSize(delta) { let index = sizes.indexOf(currentSize); index = Math.max(0, Math.min(sizes.length - 1, index + delta)); currentSize = sizes[index]; localStorage.setItem(SIZE_KEY, currentSize); applySettings(); } function applySettings() { // 1. Reset theme classes and apply selected themes.forEach(t => readerCard.classList.remove('reader-theme-' + t)); readerCard.classList.add('reader-theme-' + currentTheme); // Update Theme circles highlight themes.forEach(t => { const circle = document.getElementById('theme-' + t); if (circle) { if (t === currentTheme) { circle.classList.add('active'); } else { circle.classList.remove('active'); } } }); // 2. Reset Font classes and apply selected fonts.forEach(f => readerCard.classList.remove('reader-font-' + f)); readerCard.classList.add('reader-font-' + currentFont); // Highlight Font buttons const serifBtns = [document.getElementById('btn-font-serif'), document.getElementById('sidebar-font-serif')]; const sansBtns = [document.getElementById('btn-font-sans'), document.getElementById('sidebar-font-sans')]; serifBtns.forEach(btn => { if (btn) { if (currentFont === 'serif') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); sansBtns.forEach(btn => { if (btn) { if (currentFont === 'sans') { btn.classList.add('btn-primary-custom'); btn.classList.remove('btn-light'); } else { btn.classList.add('btn-light'); btn.classList.remove('btn-primary-custom'); } } }); // 3. Reset Sizing classes and apply selected sizes.forEach(s => readerCard.classList.remove('reader-size-' + s)); readerCard.classList.add('reader-size-' + currentSize); } // Reading Scroll progress calculation const progressBar = document.getElementById('reading-progress-bar'); window.addEventListener('scroll', () => { const documentHeight = document.documentElement.scrollHeight - window.innerHeight; if (documentHeight > 0) { const scrollProgress = (window.pageYOffset / documentHeight) * 100; progressBar.style.width = scrollProgress + '%'; } }); }); </script> <script> // ══════════════════════════════════════════════════════════════ // PWA: Service Worker registration + install prompt // ══════════════════════════════════════════════════════════════ (function () { const PWA_VERSION = '20260805-1'; // ---- Service worker ------------------------------------ if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/sw.js?v=' + PWA_VERSION).then((reg) => { // Apply updates (new deploy) on the next load without disruption reg.addEventListener('updatefound', () => { const newWorker = reg.installing; if (!newWorker) return; newWorker.addEventListener('statechange', () => { if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { newWorker.postMessage({ type: 'SKIP_WAITING' }); } }); }); }).catch(() => {}); }); } // ---- Install prompt -------------------------------------- let deferredPrompt = null; const bannerId = 'pwa-install-banner'; const DISMISS_KEY = 'nesh-install-dismissed'; function showInstallBanner() { if (localStorage.getItem(DISMISS_KEY) || document.getElementById(bannerId)) return; const banner = document.createElement('div'); banner.id = bannerId; banner.setAttribute('role', 'dialog'); banner.setAttribute('aria-label', 'Install app'); banner.innerHTML = '<div class="pwa-install-content">' + '<span class="pwa-install-icon">' + '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>' + '</span>' + '<div class="pwa-install-text">' + '<strong>Install NeshDevTech</strong>' + '<span>Get the app on your device</span>' + '</div>' + '<button type="button" class="pwa-install-btn" id="pwa-install-btn">Install</button>' + '<button type="button" class="pwa-install-close" id="pwa-install-close" aria-label="Dismiss">×</button>' + '</div>'; document.body.appendChild(banner); document.getElementById('pwa-install-btn').addEventListener('click', () => { if (!deferredPrompt) return; deferredPrompt.prompt(); deferredPrompt.userChoice.finally(() => { deferredPrompt = null; hideInstallBanner(); }); }); document.getElementById('pwa-install-close').addEventListener('click', () => { localStorage.setItem(DISMISS_KEY, '1'); hideInstallBanner(); }); requestAnimationFrame(() => banner.classList.add('show')); } function hideInstallBanner() { const banner = document.getElementById(bannerId); if (banner) { banner.classList.remove('show'); setTimeout(() => banner.remove(), 250); } } window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; showInstallBanner(); }); window.addEventListener('appinstalled', () => { deferredPrompt = null; hideInstallBanner(); localStorage.setItem(DISMISS_KEY, '1'); }); })(); </script> <style> .pwa-install-content { display: flex; align-items: center; gap: 12px; padding: 14px 18px; } #pwa-install-banner { position: fixed; left: 50%; transform: translate(-50%, 20px); bottom: 84px; z-index: 1070; background: var(--surface); color: var(--text-primary); border: 1px solid var(--border); border-radius: 18px; box-shadow: var(--shadow-lg); opacity: 0; transition: opacity .25s cubic-bezier(.16, 1, .3, 1), transform .25s cubic-bezier(.16, 1, .3, 1); width: min(420px, calc(100vw - 32px)); } #pwa-install-banner.show { opacity: 1; transform: translate(-50%, 0); } .pwa-install-icon { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; flex: 0 0 40px; border-radius: 12px; background: rgba(var(--primary-rgb), 0.1); border: 1px solid rgba(var(--primary-rgb), 0.2); color: var(--primary-color); } .pwa-install-icon svg { width: 20px; height: 20px; } .pwa-install-text { display: flex; flex-direction: column; flex: 1; min-width: 0; } .pwa-install-text strong { font-size: 0.92rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pwa-install-text span { font-size: 0.78rem; color: var(--text-secondary); } .pwa-install-btn { background: var(--primary-color); border: none; color: #fff; font-weight: 600; font-size: 0.85rem; padding: 8px 18px; border-radius: 9999px; cursor: pointer; white-space: nowrap; box-shadow: 0 4px 12px rgba(var(--primary-rgb), 0.25); transition: all .2s ease; } .pwa-install-btn:hover { background: var(--primary-dark); transform: translateY(-1px); } .pwa-install-close { background: transparent; border: none; color: var(--text-muted); font-size: 1.4rem; line-height: 1; cursor: pointer; padding: 2px 6px; border-radius: 50%; transition: background-color .2s ease, color .2s ease; } .pwa-install-close:hover { background: var(--surface-alt); color: var(--text-primary); } @media (min-width: 992px) { #pwa-install-banner { bottom: 24px; } } </style> </div><!-- /.page-wrapper --> </body> </html>