index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>マニュアル</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="header-placeholder"></div>
<div class="page-wrapper">
<!-- ★ JSで中身を描画する -->
<div id="sidebar-placeholder"></div>
<div class="content-wrapper">
<div class="main">
<h3 class="head-i">その他</h3>
<a href="https://chatgpt.com/">ChatGPT</a>
<h3 class="head-i">データ・ツール</h3>
<a href="https://chatgpt.com/">ChatGPT</a>
</div>
</div>
</div>
<script src="favorites-data.js"></script>
<script src="load_header.js"></script>
<script src="load_sidebar.js"></script>
</body>
</html>
load_header.js
document.addEventListener('DOMContentLoaded', () => {
/* ===============================
コンテンツ拡張状態の復元
=============================== */
const savedLayout = localStorage.getItem('layout-state');
if (savedLayout === 'expanded') {
document.body.classList.add('layout-expanded');
}
/* ===============================
リンク解決(file/http 両対応)
=============================== */
const path = window.location.pathname;
// index.html かどうかで判定
const isRoot = path.endsWith('/index.html');
// ベースパス
const base = isRoot ? '' : '../';
// 各リンク
const homeLink = base + 'index.html';
const blogLink = base + 'blog/blog.html';
const sonotaLink = base + 'sonota/sonota.html';
const zakkiLink = base + 'zakki/zakki.html';
const gijyutuLink = base + 'gijyutu/gijyutu.html';
/*───────────────────────────────────────────────
2. Windows パスを取得(UNC 対応)
‑ file:///D:/foo/bar.html → D:\foo\bar.html
‑ file://server/share/foo → \\server\share\foo\bar.html
───────────────────────────────────────────────*/
function getWindowsFullPath () {
const url = new URL(window.location.href);
if (url.protocol !== 'file:') return '';
// UNC (file://server/share/...)
if (url.host) {
return '\\\\' + url.host + decodeURIComponent(url.pathname).replace(/\//g, '\\');
}
// ローカルドライブ (file:///D:/...)
return decodeURIComponent(url.pathname)
.replace(/\//g, '\\') // / → \
.replace(/^\\/, ''); // 先頭の \ を除去
}
const fullPathWin = getWindowsFullPath(); // 例: D:\work\index.html or \\server\share\index.html
const dirPathWin = fullPathWin.substring(0, fullPathWin.lastIndexOf('\\'));
/*───────────────────────────────────────────────
メモ帳用 URI を生成
───────────────────────────────────────────────*/
const notepadFileUri = `note:${fullPathWin}`;
const notepadFolderUri= `note:${dirPathWin}`;
/* ===============================
1. ヘッダー描画
=============================== */
document.getElementById('header-placeholder').innerHTML = `
<header class="site-header">
<div class="header-inner">
<div class="header-left">
<button class="header-btn header-menu-btn"
id="toggle-sidebar">☰</button>
<a href="${homeLink}" class="site-logo">manual</a>
</div>
<nav class="main-nav">
<ul>
<li><a href="${sonotaLink}">その他</a></li>
<li><a href="${blogLink}">ブログ</a></li>
<li><a href="${zakkiLink}">雑記</a></li>
<li><a href="${gijyutuLink}">技術</a></li>
</ul>
</nav>
<div class="header-right">
<a href="${notepadFileUri}"
class="header-btn"
title="このファイルをメモ帳で開く"
style="margin-right:0; color:#fff; text-decoration:none;">
<svg width="18" height="18"
viewBox="0 0 24 24"
fill="none"
aria-hidden="true">
<rect x="3" y="3" width="18" height="18" rx="2"
stroke="currentColor" stroke-width="2"/>
<line x1="7" y1="8" x2="17" y2="8"
stroke="currentColor" stroke-width="2"/>
<line x1="7" y1="12" x2="17" y2="12"
stroke="currentColor" stroke-width="2"/>
<line x1="7" y1="16" x2="13" y2="16"
stroke="currentColor" stroke-width="2"/>
</svg>
</a>
<button class="header-btn"
id="open-current-folder"
title="このページのフォルダを開く">
📁
</button>
<button class="memo-toggle"
aria-label="メモを開く"
title="メモ">
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8 4h8a2 2 0 0 1 2 2v7.17a2 2 0 0 1-.59 1.41l-3.83 3.83A2 2 0 0 1 12.17 19H8a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z"
fill="currentColor"/>
<path d="M13 19v-3a2 2 0 0 1 2-2h3"
fill="none"
stroke="currentColor"
stroke-width="2"/>
</svg>
</button>
</div>
</div>
</header>
`;
/* ===============================
2. スライドメニュー生成
=============================== */
document.body.insertAdjacentHTML('beforeend', `
<nav class="slide-menu" id="slide-menu">
<button class="expand-btn" id="toggle-layout">
コンテンツを広げる
</button>
</nav>
<div class="menu-overlay" id="menu-overlay"></div>
`);
/* ===============================
3. 開閉処理
=============================== */
const menuBtn = document.getElementById('toggle-sidebar');
const overlay = document.getElementById('menu-overlay');
const layoutBtn = document.getElementById('toggle-layout');
menuBtn.addEventListener('click', () => {
document.body.classList.toggle('menu-open');
});
overlay.addEventListener('click', () => {
document.body.classList.remove('menu-open');
});
layoutBtn.addEventListener('click', () => {
const isExpanded = document.body.classList.toggle('layout-expanded');
// 状態を保存
localStorage.setItem(
'layout-state',
isExpanded ? 'expanded' : 'normal'
);
// メニューを閉じる
document.body.classList.remove('menu-open');
});
/* ===============================
現在のページのフォルダを開く
=============================== */
const openFolderBtn = document.getElementById('open-current-folder');
if (openFolderBtn) {
openFolderBtn.addEventListener('click', () => {
// file:// 以外では無効
if (location.protocol !== 'file:') {
alert('この機能はローカル環境専用です');
return;
}
// file:///D:/path/to/file.html → D:/path/to/
let folderPath = decodeURIComponent(location.pathname);
// Windows用に先頭の / を除去
if (folderPath.startsWith('/')) {
folderPath = folderPath.slice(1);
}
// ファイル名を削除
folderPath = folderPath.replace(/\/[^\/]+$/, '');
// explorer で開く
location.href = 'explorer:' + folderPath;
});
}
/* ===============================
クイックメモ
=============================== */
// ===== DOM 生成 =====
const memoPanel = document.createElement('aside');
memoPanel.id = 'memo-panel';
memoPanel.innerHTML = `
<div class="memo-header">
<div class="memo-tabs">
<button class="memo-tab" data-tab="1">1</button>
<button class="memo-tab" data-tab="2">2</button>
<button class="memo-tab" data-tab="3">3</button>
</div>
<div class="memo-actions">
<button id="memo-expand" title="広げる">⤢</button>
<button id="memo-close" title="閉じる">×</button>
</div>
</div>
<textarea id="memo-text"
placeholder="ここにメモを書けます(自動保存)"></textarea>
`;
document.body.appendChild(memoPanel);
// ===== 要素取得 =====
const memoToggleBtn = document.querySelector('.memo-toggle');
const memoCloseBtn = document.getElementById('memo-close');
const memoTextEl = document.getElementById('memo-text');
const memoExpandBtn = document.getElementById('memo-expand');
// ===== localStorage key =====
const MEMO_OPEN_KEY = 'quickMemoOpen';
const MEMO_EXPAND_KEY = 'quickMemoExpanded';
/* ===============================
メモタブ管理
=============================== */
const MEMO_TAB_KEY = 'quickMemoCurrentTab';
let currentTab = localStorage.getItem(MEMO_TAB_KEY) || '1';
const tabButtons = memoPanel.querySelectorAll('.memo-tab');
let isInitialLoad = true;
function switchTab(tabId) {
if (!isInitialLoad) {
localStorage.setItem(
`quickMemoText_tab${currentTab}`,
memoTextEl.value || ''
);
}
currentTab = tabId;
localStorage.setItem(MEMO_TAB_KEY, tabId);
memoTextEl.value =
localStorage.getItem(`quickMemoText_tab${tabId}`) || '';
tabButtons.forEach(btn =>
btn.classList.toggle('active', btn.dataset.tab === tabId)
);
isInitialLoad = false;
}
// ★ これが抜けていた
tabButtons.forEach(btn => {
btn.addEventListener('click', () => {
switchTab(btn.dataset.tab);
});
});
memoExpandBtn?.addEventListener('click', () => {
const isExpanded = memoPanel.classList.toggle('expanded');
memoExpandBtn.textContent = isExpanded ? '⤡' : '⤢';
localStorage.setItem(MEMO_EXPAND_KEY, String(isExpanded));
});
// ===== 状態復元(初回チラつき防止) =====
(function restoreMemo() {
const open = localStorage.getItem(MEMO_OPEN_KEY) === 'true';
memoPanel.classList.toggle('active', open);
memoToggleBtn?.classList.toggle('is-active', open);
const expanded = localStorage.getItem(MEMO_EXPAND_KEY) === 'true';
memoPanel.classList.toggle('expanded', expanded);
memoExpandBtn.textContent = expanded ? '⤡' : '⤢';
// ★ タブ状態+本文は switchTab に任せる
switchTab(currentTab);
})();
// ===== 開閉処理 =====
function setMemoOpen(next) {
memoPanel.classList.toggle('active', next);
memoToggleBtn?.classList.toggle('is-active', next); // ★ 追加
localStorage.setItem(MEMO_OPEN_KEY, String(next));
}
memoToggleBtn?.addEventListener('click', () => {
const willOpen = !memoPanel.classList.contains('active');
setMemoOpen(willOpen);
});
memoCloseBtn?.addEventListener('click', () => {
setMemoOpen(false);
});
// ===== 自動保存(300ms デバウンス) =====
let saveTimer = null;
memoTextEl.addEventListener('input', () => {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
localStorage.setItem(
`quickMemoText_tab${currentTab}`,
memoTextEl.value || ''
);
}, 300);
});
});
load_sidebar.js
document.addEventListener('DOMContentLoaded', () => {
/* ===============================
1. サイドバー描画
=============================== */
const sidebarHTML = `
<aside class="sidebar" id="sidebar">
<div class="sidebar-box">
<h3>📁 フォルダ</h3>
<ul>
<li>
<a href="explorer:D:\\kyoyubunsyo\\program" class="flink">
プログラム開発フォルダ
</a>
</li>
</ul>
</div>
<div class="sidebar-box">
<h3>⭐ お気に入り</h3>
<ul id="favorites-list"></ul>
</div>
<div class="sidebar-box">
<h3>💻️ メニュー</h3>
<ul>
<li>
<a href="#" id="open-notepad-link" class="flink">
このページをメモ帳で開く
</a>
</li>
</ul>
</div>
</aside>
<div id="toast" class="toast">処理中…</div>
`;
document.getElementById('sidebar-placeholder').innerHTML = sidebarHTML;
/* ===============================
2. サイドバー開閉
=============================== */
const btn = document.getElementById('toggle-sidebar');
const sidebar = document.getElementById('sidebar');
if (btn && sidebar) {
btn.addEventListener('click', () => {
sidebar.classList.toggle('is-hidden');
});
}
/* ===============================
flink → トースト表示
=============================== */
const toast = document.getElementById('toast');
function showToast(message, duration = 1500) {
if (!toast) return;
toast.textContent = message;
toast.classList.add('is-show');
setTimeout(() => {
toast.classList.remove('is-show');
}, duration);
}
/* flink クリック */
document.addEventListener('click', (e) => {
const link = e.target.closest('.flink');
if (!link) return;
// トースト表示
showToast('処理中…');
// explorer: リンクはそのまま動かす
});
/* ===============================
メモ帳で開くリンク
=============================== */
const notepadLink = document.getElementById('open-notepad-link');
if (notepadLink) {
notepadLink.addEventListener('click', (e) => {
e.preventDefault();
// file:// 以外では無効
if (location.protocol !== 'file:') {
alert('この機能はローカル環境専用です');
return;
}
// file:///D:/path/to/file.html → D:\path\to\file.html
let fullPath = decodeURIComponent(location.pathname);
if (fullPath.startsWith('/')) {
fullPath = fullPath.slice(1);
}
fullPath = fullPath.replace(/\//g, '\\');
// トースト
showToast('メモ帳で開いています…');
// メモ帳で開く
location.href = 'note:' + fullPath;
});
}
});
style.css
@charset "UTF-8";
:root {
--bg-main: #f4f6f8;
--bg-sidebar: #ffffff;
--border-soft: #e3e6ea;
--text-main: #2b2f33;
--text-sub: #6b7280;
--accent: #2563eb;
--hover-bg: #f1f5ff;
}
/* =========================================
全体の定義
========================================= */
body {
margin: 0;
font-family: "Segoe UI", "Noto Sans JP", system-ui, sans-serif;
color: var(--text-main);
background: var(--bg-main);
}
.page-wrapper {
display: flex;
flex-direction: row-reverse; /* 右サイドバー */
max-width: 1200px; /* ★ ayamanual感の核 */
margin: 32px auto; /* ★ 中央寄せ+上下余白 */
gap: 32px; /* ★ メインとサイドの距離 */
}
.content-wrapper {
flex: 1;
background: #ffffff;
padding: 32px 36px;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.main {
max-width: 900px;
}
.head-i {
font-size: 18px;
margin: 26px 0 12px;
padding-left: 10px;
border-left: 4px solid var(--accent);
}
.main a {
color: var(--accent);
text-decoration: none;
}
.main a:hover {
text-decoration: underline;
}
/* =========================
コンテンツ拡張モード
========================= */
body.layout-expanded .page-wrapper {
display: flex;
flex-direction: column;
max-width: 900px;
margin: 32px auto;
gap: 24px;
}
body.layout-expanded .content-wrapper {
order: 1;
width: 100%;
border-radius: 0;
box-shadow: none;
}
body.layout-expanded #sidebar-placeholder {
order: 2;
}
body.layout-expanded .main {
max-width: none;
}
/* ← この下に追加 */
body.layout-expanded .sidebar {
width: 100%;
}
/* ワンカラム時:上下カードの余白を統一 */
body.layout-expanded .content-wrapper,
body.layout-expanded .sidebar {
padding: 32px 36px;
}
/* =========================================
ヘッダーの定義
========================================= */
.site-header {
background: #222;
height: 56px;
color: #fff;
}
.header-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
height: 100%;
display: flex;
align-items: center;
}
.site-logo {
font-size: 20px;
font-weight: 700;
color: #fff;
text-decoration: none;
margin-right: 32px;
}
.site-logo:hover {
opacity: 0.85;
}
.main-nav ul {
display: flex; /* 横並び */
gap: 24px; /* メニュー間隔 */
list-style: none; /* ● を消す */
margin: 0;
padding: 0;
}
.main-nav li {
margin: 0; /* liの余計な余白を消す */
padding: 0;
}
.main-nav a {
color: #e5e7eb;
text-decoration: none;
font-size: 14px;
line-height: 56px; /* ヘッダー中央揃え */
}
.main-nav a:hover {
color: #ffffff;
}
.header-right {
margin-left: auto;
}
.header-btn {
background: transparent;
border: none;
color: #fff;
font-size: 20px;
cursor: pointer;
}
.header-right {
margin-left: auto;
display: flex; /* ★ 追加 */
align-items: center; /* ★ 上下中央 */
gap: 10px; /* ★ アイコン間隔 */
}
.header-btn {
display: inline-flex; /* ★ 追加 */
align-items: center; /* ★ 上下中央 */
justify-content: center;
line-height: 1; /* ★ 絵文字ズレ防止 */
}
/* ===== ヘッダーは強制的に横書き ===== */
.site-header,
.site-header * {
writing-mode: horizontal-tb !important;
text-orientation: mixed !important;
white-space: nowrap;
}
/* SVGのベースライン問題を解消 */
.header-btn svg {
display: block;
}
/* ===============================
クイックメモ
=============================== */
#memo-panel {
position: fixed;
top: 56px; /* ヘッダー高さ */
right: 0;
width: 420px;
height: calc(100vh - 56px);
background: #1f2937;
color: #fff;
box-shadow: -4px 0 12px rgba(0, 0, 0, 0.25);
transform: translateX(100%);
transition: transform 0.25s ease;
z-index: 1000;
display: flex;
flex-direction: column;
}
#memo-panel.active {
transform: translateX(0);
}
/* ★ 広げた状態 */
#memo-panel.expanded {
width: 920px;
}
.memo-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 12px;
background: #111827;
font-size: 13px;
}
.memo-header button {
background: transparent;
border: none;
color: #fff;
font-size: 18px;
cursor: pointer;
}
.memo-actions button {
background: none;
border: none;
color: #fff;
cursor: pointer;
font-size: 16px;
margin-left: 6px;
}
#memo-text {
flex: 1;
background: transparent;
border: none;
color: #fff;
padding: 10px;
resize: none;
outline: none;
font-size: 14px;
line-height: 1.6;
}
/* ===============================
ヘッダーアイコン共通
=============================== */
.header-btn,
.memo-toggle {
background: transparent;
border: none;
color: #fff;
padding: 6px;
border-radius: 6px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: background 0.15s ease, opacity 0.15s ease;
}
/* SVGのズレ防止 */
.header-btn svg,
.memo-toggle svg {
display: block;
}
/* hover */
.header-btn:hover,
.memo-toggle:hover {
background: rgba(255, 255, 255, 0.12);
}
/* active(押した感) */
.header-btn:active,
.memo-toggle:active {
background: rgba(255, 255, 255, 0.22);
}
/* ===============================
メモON状態(開いている時)
=============================== */
.memo-toggle.is-active {
background: rgba(255, 255, 255, 0.25);
}
/* ===============================
メモタブ
=============================== */
.memo-tabs {
display: flex;
gap: 4px;
}
.memo-tab {
background: transparent;
border: none;
color: #9ca3af;
padding: 2px 6px;
font-size: 12px;
border-radius: 4px;
cursor: pointer;
}
.memo-tab.active {
background: rgba(255, 255, 255, 0.25);
color: #fff;
}
/* =========================================
サイドバーの定義
========================================= */
.sidebar {
width: 260px;
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 2px;
padding: 20px 18px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.sidebar-box {
margin-bottom: 26px;
}
.sidebar-box h3 {
font-size: 13px;
font-weight: 600;
color: var(--text-sub);
margin-bottom: 10px;
letter-spacing: 0.03em;
}
.sidebar-box ul {
list-style: none;
padding: 0;
margin: 0;
}
.sidebar-box li {
margin-bottom: 4px;
}
.sidebar-box a {
display: block;
padding: 8px 10px;
border-radius: 8px;
text-decoration: none;
color: var(--text-main);
font-size: 14px;
transition: background 0.15s ease, color 0.15s ease;
}
.sidebar-box a:hover {
background: var(--hover-bg);
color: var(--accent);
}
/* =========================
スライドメニュー本体
========================= */
.slide-menu {
position: fixed;
top: 0;
left: 0;
width: 260px;
height: 100vh;
background: #111;
color: #fff;
padding: 40px 20px 20px;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1000;
}
.slide-menu ul {
list-style: none;
padding: 0;
margin: 0;
}
.slide-menu li {
margin-bottom: 12px;
}
.slide-menu a {
color: #e5e7eb;
text-decoration: none;
font-size: 16px;
}
.slide-menu a:hover {
color: #fff;
}
/* =========================
背景オーバーレイ
========================= */
.menu-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 900;
}
/* =========================
開いた状態
========================= */
body.menu-open .slide-menu {
transform: translateX(0);
}
body.menu-open .menu-overlay {
opacity: 1;
pointer-events: auto;
}
.expand-btn {
margin-top: 24px;
width: 100%;
padding: 10px 12px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
.expand-btn:hover {
background: #1e4fd8;
}
/* ===== トースト ===== */
.toast {
position: fixed;
left: 50%;
bottom: 28px; /* ← 少し下に余白 */
transform: translateX(-50%) translateY(30px);
background: rgba(0, 0, 0, 0.88);
color: #fff;
padding: 12px 18px 14px; /* ← 下を少し厚めに */
border-radius: 8px;
font-size: 13px;
letter-spacing: 0.02em;
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
z-index: 9999;
}
.toast.is-show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ==========================
スマホ:1カラム&横いっぱい
========================== */
@media (max-width: 900px) {
.page-wrapper {
flex-direction: column;
max-width: none;
margin: 0;
gap: 0;
}
/* コンテンツを先に */
.content-wrapper {
order: 1;
width: 100%;
margin: 0;
border-radius: 0;
box-shadow: none;
padding: 20px 16px;
}
/* ★ ここが重要 */
#sidebar-placeholder {
order: 2;
}
.sidebar {
width: 100%;
margin: 0;
border-radius: 0;
box-shadow: none;
border-left: none;
border-top: 1px solid #e5e7eb;
padding: 16px;
}
}