daydreamer-json commited on
Commit
d7d98b9
·
verified ·
1 Parent(s): dc024d1

Test build

Browse files
Files changed (3) hide show
  1. index.html +126 -17
  2. script.js +238 -0
  3. style.css +4 -26
index.html CHANGED
@@ -1,19 +1,128 @@
1
  <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!DOCTYPE html>
2
+ <html lang="en" data-bs-theme="dark">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>discography_v2_cdn_front</title>
6
+ <!-- Responsive viewport meta -->
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <!-- JavaScript load -->
9
+ <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js"></script>
11
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/js/bootstrap.bundle.min.js"></script>
12
+ <!-- CSS load -->
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5/dist/css/bootstrap.min.css">
14
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.min.css">
15
+ <link rel="preconnect" href="https://rsms.me">
16
+ <link rel="stylesheet" href="https://rsms.me/inter/inter.css">
17
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/distr/fira_code.css">
18
+ <link rel="preconnect" href="https://fonts.googleapis.com">
19
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
20
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&display=swap">
21
+ <link rel="stylesheet" href="style.css">
22
+ </head>
23
+ <body>
24
+ <div id="mainContainer" class="container my-4">
25
+ <h1 class="user-select-none text-center">discography_v2_cdn_front</h1>
26
+ <hr class="my-4">
27
+ <button id="showSettingsModalButton" class="btn btn-primary" type="button" data-bs-toggle="modal" data-bs-target="#settingsModal"><i class="bi bi-gear-wide-connected me-1"></i>Settings</button>
28
+ <div class="modal fade user-select-none" id="settingsModal" tabindex="-1">
29
+ <div class="modal-dialog modal-dialog-scrollable">
30
+ <div class="modal-content">
31
+ <div class="modal-header">
32
+ <h1 class="modal-title fs-5" id="settingsModalLabel"><i class="bi bi-gear-wide-connected me-2"></i>Settings</h1>
33
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
34
+ </div>
35
+ <div class="modal-body">
36
+ <!-- <ul class="nav nav-tabs" id="myTab" role="tablist">
37
+ <li class="nav-item" role="presentation">
38
+ <button class="nav-link active" id="home-tab" data-bs-toggle="tab" data-bs-target="#home-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">Home</button>
39
+ </li>
40
+ <li class="nav-item" role="presentation">
41
+ <button class="nav-link" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-tab-pane" type="button" role="tab" aria-controls="profile-tab-pane" aria-selected="false">Profile</button>
42
+ </li>
43
+ <li class="nav-item" role="presentation">
44
+ <button class="nav-link" id="contact-tab" data-bs-toggle="tab" data-bs-target="#contact-tab-pane" type="button" role="tab" aria-controls="contact-tab-pane" aria-selected="false">Contact</button>
45
+ </li>
46
+ <li class="nav-item" role="presentation">
47
+ <button class="nav-link" id="disabled-tab" data-bs-toggle="tab" data-bs-target="#disabled-tab-pane" type="button" role="tab" aria-controls="disabled-tab-pane" aria-selected="false" disabled>Disabled</button>
48
+ </li>
49
+ </ul>
50
+ <div class="tab-content" id="myTabContent">
51
+ <div class="tab-pane fade show active" id="home-tab-pane" role="tabpanel" aria-labelledby="home-tab" tabindex="0">...</div>
52
+ <div class="tab-pane fade" id="profile-tab-pane" role="tabpanel" aria-labelledby="profile-tab" tabindex="0">...</div>
53
+ <div class="tab-pane fade" id="contact-tab-pane" role="tabpanel" aria-labelledby="contact-tab" tabindex="0">...</div>
54
+ <div class="tab-pane fade" id="disabled-tab-pane" role="tabpanel" aria-labelledby="disabled-tab" tabindex="0">...</div>
55
+ </div> -->
56
+ <h1 class="fs-3">UI</h1>
57
+ <form id="settingsUiSwitchUiMode" class="d-flex justify-content-between align-items-center">
58
+ <label>Switch UI mode</label>
59
+ <div class="btn-group w-100%" id="settingsUiSwitchUiModeDiv" role="group">
60
+ <input type="radio" class="btn-check" name="settingsUiSwitchUiMode" id="settingsUiSwitchUiModeLight" autocomplete="off" value="light">
61
+ <label class="btn btn-outline-primary" for="settingsUiSwitchUiModeLight">Light</label>
62
+ <input type="radio" class="btn-check" name="settingsUiSwitchUiMode" id="settingsUiSwitchUiModeDark" autocomplete="off" value="dark">
63
+ <label class="btn btn-outline-primary" for="settingsUiSwitchUiModeDark">Dark</label>
64
+ </div>
65
+ </form>
66
+ </div>
67
+ <div class="modal-footer">
68
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
69
+ <button type="button" class="btn btn-primary" id="settingsApplyButton">Save/Apply</button>
70
+ </div>
71
+ </div>
72
+ </div>
73
+ </div>
74
+ <hr class="my-4">
75
+ <h3 class="user-select-none">Track listing test</h3>
76
+ <button id="trackListingTestButton" class="btn btn-primary mb-3" type="button"><i class="bi bi-menu-button-wide me-1"></i>List track</button>
77
+ <div class="fetchingDataNowLabel user-select-none d-none">
78
+ <div class="d-inline-flex align-items-center mb-3">
79
+ <div class="spinner-border text-primary me-2" role="status">
80
+ <span class="visually-hidden">Loading...</span>
81
+ </div>
82
+ <span>Fetching data ...</span>
83
+ </div>
84
+ </div>
85
+ <div id="trackAllListGroup" class="list-group user-select-none">
86
+ <button type="button" class="list-group-item list-group-item-action d-flex" disabled>
87
+ <div class="flex-fill w-100">Artist</div>
88
+ <div class="flex-fill w-100">Album</div>
89
+ <div class="flex-fill w-100">Track</div>
90
+ </button>
91
+ </div>
92
+ <hr class="my-4">
93
+ <h3 class="user-select-none">Fetch data output testing</h3>
94
+ <button id="loadDatabaseTestButton" class="btn btn-primary mb-3" type="button"><i class="bi bi-database-down me-1"></i>Load Database</button>
95
+ <div class="fetchingDataNowLabel user-select-none d-none">
96
+ <div class="d-inline-flex align-items-center mb-3">
97
+ <div class="spinner-border text-primary me-2" role="status">
98
+ <span class="visually-hidden">Loading...</span>
99
+ </div>
100
+ <span>Fetching data ...</span>
101
+ </div>
102
+ </div>
103
+ <div class="accordion" id="databaseTestOutputAccordion">
104
+ <div class="accordion-item">
105
+ <h2 class="accordion-header">
106
+ <button class="accordion-button collapsed user-select-none" type="button" data-bs-toggle="collapse" data-bs-target="#databaseTestRawOutputCollapse">
107
+ Raw data output
108
+ </button>
109
+ </h2>
110
+ <div id="databaseTestRawOutputCollapse" class="accordion-collapse collapse" data-bs-parent="#databaseTestOutputAccordion">
111
+ <div class="accordion-body">
112
+ <pre class=""><code id="databaseTestInfoCodeEl"></code></pre>
113
+ <pre class=""><code id="databaseTestOutputCodeEl"></code></pre>
114
+ </div>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <hr class="my-4">
119
+ <iframe data-aa='2297053' src='//acceptable.a-ads.com/2297053' style='border:0px; padding:0; width:100%; height:100%; overflow:hidden; background-color: transparent;'></iframe>
120
+ <hr class="my-4">
121
+ <footer>
122
+ <p><small class="text-body-tertiary user-select-none">This page using: Bootstrap, Axios, CryptoJS, A-ADS, Inter, Fira Code, Noto Sans JP</small></p>
123
+ <p><small class="text-body-tertiary user-select-none">(C) 2024 daydreamer-json, and other contributer. All rights reserved.</small></p>
124
+ </footer>
125
+ </div>
126
+ <script src="script.js"></script>
127
+ </body>
128
  </html>
script.js ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const internalConfig = {
2
+ 'network': {
3
+ 'userAgent': {
4
+ 'chromeWindows': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
5
+ 'curl': 'curl/8.4.0',
6
+ 'curlUnity': 'UnityPlayer/2021.3.14f1 (UnityWebRequest/1.0, libcurl/7.84.0-DEV)'
7
+ },
8
+ 'timeout': 15000
9
+ }
10
+ };
11
+ const apiConnectDefaultHeader = {
12
+ 'Access-Controll-Allow-Origin': '*'
13
+ }
14
+ const appSettingsStorageName = '018d2fc7-d0bb-7393-9ebc-6f6ec26b03ce_appSettings';
15
+ let appSettingsSaveData = new Object();
16
+ const appSettingsSaveDataDefault = {
17
+ 'ui': {
18
+ 'uiThemeMode': 'light'
19
+ }
20
+ };
21
+ let apiDataMasterDB = new Object();
22
+ let apiDataConfig = new Object();
23
+
24
+ //!========== ページ読み込み時に実行する処理 ==========
25
+
26
+ window.addEventListener('load', async function(){
27
+ if (checkAppSettingsExistsOnStorage() === true) {
28
+ loadAppSettingsFromLocalStorage();
29
+ } else {
30
+ loadAppSettingsFromLocalStorage();
31
+ if (window.matchMedia('(prefers-color-scheme:dark)').matches === true) {
32
+ console.warn(`prefers-color-scheme:dark detected`)
33
+ appSettingsSaveData.ui.uiThemeMode = 'dark';
34
+ writeAppSettingsToLocalStorage();
35
+ } else {
36
+ appSettingsSaveData.ui.uiThemeMode = 'light';
37
+ writeAppSettingsToLocalStorage();
38
+ }
39
+ }
40
+ appSettingsCheckedUiUpdate();
41
+ appSettingsApply();
42
+ await loadRequiredDatabase();
43
+ await decryptConfig();
44
+ pushToTrackListGroupUi();
45
+ });
46
+
47
+ document.querySelector('#loadDatabaseTestButton').addEventListener('click', async function() {
48
+ console.log('loadDatabaseTestButton clicked!');
49
+ await loadRequiredDatabase();
50
+ });
51
+
52
+ document.querySelector('#trackListingTestButton').addEventListener('click', () => {
53
+ console.log('trackListingTestButton clicked!');
54
+ pushToTrackListGroupUi();
55
+ });
56
+
57
+ // ========== 全てのトラックのリストをTrack Listing TestのListGroupに表示 ==========
58
+
59
+ function pushToTrackListGroupUi () {
60
+ const trackAllListGroup = document.querySelector('#trackAllListGroup');
61
+ while(trackAllListGroup.firstChild) {
62
+ trackAllListGroup.removeChild(trackAllListGroup.firstChild);
63
+ }
64
+ let headerElObj = new Object();
65
+ headerElObj.buttonNodeEl = document.createElement('button');
66
+ headerElObj.divArtistNodeEl = document.createElement('div');
67
+ headerElObj.divAlbumNodeEl = document.createElement('div');
68
+ headerElObj.divTrackNodeEl = document.createElement('div');
69
+ headerElObj.buttonNodeEl.classList.add('list-group-item', 'list-group-item-action', 'd-flex');
70
+ headerElObj.buttonNodeEl.setAttribute('type', 'button');
71
+ headerElObj.buttonNodeEl.disabled = true;
72
+ headerElObj.divArtistNodeEl.classList.add('flex-fill', 'w-100');
73
+ headerElObj.divAlbumNodeEl.classList.add('flex-fill', 'w-100');
74
+ headerElObj.divTrackNodeEl.classList.add('flex-fill', 'w-100');
75
+ headerElObj.divArtistNodeEl.innerHTML = 'Artist';
76
+ headerElObj.divAlbumNodeEl.innerHTML = 'Album';
77
+ headerElObj.divTrackNodeEl.innerHTML = 'Track';
78
+ headerElObj.buttonNodeEl.appendChild(headerElObj.divArtistNodeEl);
79
+ headerElObj.buttonNodeEl.appendChild(headerElObj.divAlbumNodeEl);
80
+ headerElObj.buttonNodeEl.appendChild(headerElObj.divTrackNodeEl);
81
+ trackAllListGroup.appendChild(headerElObj.buttonNodeEl);
82
+ apiDataMasterDB.response.data.albums.forEach((albumObject) => {
83
+ albumObject.tracks.forEach((trackObject) => {
84
+ let buttonNodeEl = document.createElement('button');
85
+ let divArtistNodeEl = document.createElement('div');
86
+ let divAlbumNodeEl = document.createElement('div');
87
+ let divTrackNodeEl = document.createElement('div');
88
+ buttonNodeEl.classList.add('list-group-item', 'list-group-item-action', 'd-flex');
89
+ buttonNodeEl.setAttribute('type', 'button');
90
+ divArtistNodeEl.classList.add('flex-fill', 'w-100');
91
+ divAlbumNodeEl.classList.add('flex-fill', 'w-100');
92
+ divTrackNodeEl.classList.add('flex-fill', 'w-100');
93
+ let albumArtistFilteredList = new Array();
94
+ albumObject.artist.forEach((str) => {
95
+ albumArtistFilteredList.push(apiDataMasterDB.response.data.artists.filter((obj) => obj.uuid === str)[0].name);
96
+ });
97
+ divArtistNodeEl.innerHTML = albumArtistFilteredList.join(', ');
98
+ divAlbumNodeEl.innerHTML = albumObject.title;
99
+ divTrackNodeEl.innerHTML = trackObject.title;
100
+ buttonNodeEl.appendChild(divArtistNodeEl);
101
+ buttonNodeEl.appendChild(divAlbumNodeEl);
102
+ buttonNodeEl.appendChild(divTrackNodeEl);
103
+ trackAllListGroup.appendChild(buttonNodeEl);
104
+ });
105
+ });
106
+ }
107
+
108
+ // ========== 設定画面のイベントリスナー登録など ==========
109
+
110
+ document.querySelector('#settingsApplyButton').addEventListener('click', () => {appSettingsApply()});
111
+ document.querySelector('#settingsUiSwitchUiModeDiv').addEventListener('click', function () {
112
+ appSettingsSaveData.ui.uiThemeMode = document.querySelector('#settingsUiSwitchUiMode').elements['settingsUiSwitchUiMode'].value;
113
+ console.log('settingsUiSwitchUiMode div clicked!');
114
+ });
115
+
116
+
117
+ // ========== 設定を変更後��保存、反映させる(ボタン押したときの動作) ==========
118
+
119
+ function appSettingsApply () {
120
+ writeAppSettingsToLocalStorage();
121
+ switch (appSettingsSaveData.ui.uiThemeMode) {
122
+ case 'light':
123
+ document.querySelector('html').setAttribute('data-bs-theme', 'light');
124
+ break;
125
+ case 'dark':
126
+ document.querySelector('html').setAttribute('data-bs-theme', 'dark');
127
+ }
128
+ }
129
+
130
+ // ========== appSettingsを元に設定画面の選択状態を更新 ==========
131
+
132
+ function appSettingsCheckedUiUpdate () {
133
+ const settingsUiSwitchUiModeElements = document.querySelector('#settingsUiSwitchUiMode').elements;
134
+ for (let i = 0; i < settingsUiSwitchUiModeElements.length; i++) {
135
+ if (settingsUiSwitchUiModeElements[i].value === appSettingsSaveData.ui.uiThemeMode) {
136
+ settingsUiSwitchUiModeElements[i].checked = true;
137
+ } else {
138
+ settingsUiSwitchUiModeElements[i].checked = false;
139
+ }
140
+ }
141
+ }
142
+
143
+ // ========== API関連の関数など ==========
144
+
145
+ async function apiConnect (axiosObj) {
146
+ let connectionTimerStart = performance.now();
147
+ try {
148
+ const response = await axios(axiosObj);
149
+ let connectionTimerEnd = performance.now();
150
+ return {
151
+ 'apiConnectionTime': connectionTimerEnd - connectionTimerStart,
152
+ 'response': response.data
153
+ };
154
+ } catch (error) {
155
+ let connectionTimerEnd = performance.now();
156
+ console.error(`API request failed: ${error.code}`);
157
+ alert(`API request failed: ${error.code}`);
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ async function loadRequiredDatabase () {
163
+ document.querySelectorAll('.fetchingDataNowLabel').forEach((el) => {
164
+ el.classList.remove('d-none');
165
+ });
166
+ apiDataMasterDB = await apiConnect({
167
+ 'method': 'get',
168
+ 'url': `https://corsproxy.io/?${encodeURIComponent(`https://hf.co/datasets/DeliberatorArchiver/discography_v2_cdn/resolve/main/db/master.json`)}`,
169
+ 'headers': apiConnectDefaultHeader,
170
+ 'timeout': internalConfig.network.timeout
171
+ });
172
+ apiDataConfig = await apiConnect({
173
+ 'method': 'get',
174
+ 'url': `https://corsproxy.io/?${encodeURIComponent(`https://hf.co/spaces/DeliberatorArchiver/discography_v2_cdn_front/resolve/main/config.json`)}`,
175
+ 'headers': apiConnectDefaultHeader,
176
+ 'timeout': internalConfig.network.timeout
177
+ });
178
+ console.log(apiDataMasterDB);
179
+ console.log(apiDataConfig);
180
+ if (apiDataMasterDB) {
181
+ document.querySelector('#databaseTestInfoCodeEl').innerHTML = `OK (Time: ${Math.ceil(apiDataMasterDB.apiConnectionTime)} ms)`;
182
+ document.querySelector('#databaseTestOutputCodeEl').innerHTML = JSON.stringify(apiDataMasterDB.response, '', ' ');
183
+ } else {
184
+ document.querySelector('#databaseTestInfoCodeEl').innerHTML = `Failed`;
185
+ }
186
+ document.querySelectorAll('.fetchingDataNowLabel').forEach((el) => {
187
+ el.classList.add('d-none');
188
+ });
189
+ }
190
+
191
+ // ========== MasterDB/Configの暗号化を解く ==========
192
+
193
+ async function decryptConfig () {
194
+ apiDataConfig.response.config.decrypted = new Object();
195
+ console.log(`Decrypting config data using AES 128-bit CBC ...`)
196
+ const encryptKey = await CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(apiDataConfig.response.config.encryption.key.split('').reverse().join('')).toString(CryptoJS.enc.Utf8));
197
+ const encryptIv = await CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(apiDataConfig.response.config.encryption.iv.split('').reverse().join('')).toString(CryptoJS.enc.Utf8));
198
+ Object.keys(apiDataConfig.response.config.encrypted).forEach(async function (keyName) {
199
+ apiDataConfig.response.config.decrypted[keyName] = await CryptoJS.AES.decrypt({
200
+ 'ciphertext': CryptoJS.enc.Base64.parse(apiDataConfig.response.config.encrypted[keyName])
201
+ }, encryptKey, {
202
+ 'iv': encryptIv,
203
+ 'mode': CryptoJS.mode.CBC,
204
+ 'padding': CryptoJS.pad.Pkcs7
205
+ }).toString(CryptoJS.enc.Utf8);
206
+ });
207
+ console.log(`All config data has been decrypted`);
208
+ }
209
+
210
+ // ========== ブラウザのLocalStorageにあるAppSettingsを読み書きする ==========
211
+
212
+ function loadAppSettingsFromLocalStorage () {
213
+ if (localStorage.hasOwnProperty(appSettingsStorageName)) {
214
+ appSettingsSaveData = JSON.parse(CryptoJS.enc.Base64.parse(localStorage.getItem(appSettingsStorageName)).toString(CryptoJS.enc.Utf8));
215
+ console.warn(`LocalStorage key detected`);
216
+ console.log(`Loaded appSettings:`);
217
+ console.log(appSettingsSaveData);
218
+ } else {
219
+ appSettingsSaveData = appSettingsSaveDataDefault;
220
+ console.warn(`LocalStorage key not found\nUsing default settings`);
221
+ writeAppSettingsToLocalStorage();
222
+ }
223
+ }
224
+
225
+ function checkAppSettingsExistsOnStorage () {
226
+ if (localStorage.hasOwnProperty(appSettingsStorageName)) {
227
+ return true
228
+ } else {
229
+ return false
230
+ }
231
+ }
232
+
233
+ function writeAppSettingsToLocalStorage () {
234
+ // CryptoJS.enc.Utf8.parseを使うことでCryptoJS内部形式を強制
235
+ localStorage.setItem(appSettingsStorageName, CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(JSON.stringify(appSettingsSaveData))));
236
+ console.log(`Wrote appSettings:`);
237
+ console.log(appSettingsSaveData);
238
+ }
style.css CHANGED
@@ -1,28 +1,6 @@
1
  body {
2
- padding: 2rem;
3
- font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
4
- }
5
-
6
- h1 {
7
- font-size: 16px;
8
- margin-top: 0;
9
- }
10
-
11
- p {
12
- color: rgb(107, 114, 128);
13
- font-size: 15px;
14
- margin-bottom: 10px;
15
- margin-top: 5px;
16
- }
17
-
18
- .card {
19
- max-width: 620px;
20
- margin: 0 auto;
21
- padding: 16px;
22
- border: 1px solid lightgray;
23
- border-radius: 16px;
24
- }
25
-
26
- .card p:last-child {
27
- margin-bottom: 0;
28
  }
 
 
 
 
1
  body {
2
+ font-family: 'Inter', 'Noto Sans JP', system-ui, sans-serif
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  }
4
+ pre, code {
5
+ font-family: 'Fira Code', SFMono-Regular, SF Mono, 'Noto Sans JP', ui-monospace, monospace, sans-serif;
6
+ }