rutbhics 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zennith
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,82 @@
1
- # Temporary Holding Version
1
+ # Rutbhics
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Desktop komputer di dalam browser: window manager, taskbar, menu Start, dan
4
+ aplikasi yang berjalan langsung di halaman web — tanpa build step, tanpa
5
+ framework, murni HTML + CSS + JavaScript vanilla (seefisien mungkin).
6
+
7
+ ## Cara menjalankan
8
+
9
+ Buka `index.html` langsung di browser, atau dari terminal:
10
+
11
+ ```bash
12
+ python3 -m http.server 8000
13
+ # lalu buka http://localhost:8000
14
+ ```
15
+
16
+ ## Install lewat npm
17
+
18
+ ```bash
19
+ npm install -g rutbhics
20
+ rutbhics
21
+ # lalu buka http://localhost:8080
22
+ ```
23
+
24
+ Port bisa diubah: `rutbhics --port 3000` atau `PORT=3000 rutbhics`.
25
+ Tanpa install permanen: `npx rutbhics`.
26
+
27
+ ## Login & penggunaan sebagai server
28
+
29
+ Rutbhics bisa dijalankan sebagai server web statis (mis. `python3 -m http.server`,
30
+ Nginx, atau hosting statis apa pun) — tidak butuh backend.
31
+
32
+ - Buka halaman utama: pengunjung diarahkan ke `login.html`.
33
+ - Kunjungan pertama: buat akun perangkat (nama pengguna + kata sandi).
34
+ Akun tersimpan lokal di perangkat itu (kata sandi di-hash SHA-256 bila
35
+ konteks aman, fallback hash sederhana bila tidak).
36
+ - Setelah masuk, desktop terkunci di balik sesi tab itu; keluar lewat
37
+ tombol **Keluar** di menu Start.
38
+ - Catatan jujur: kunci ini hanya pengaman demo sisi klien, bukan
39
+ autentikasi server sungguhan. Untuk akun multi-pengguna yang aman,
40
+ diperlukan backend (mis. Node + database + sesi) di tahap berikutnya.
41
+
42
+ ## Aplikasi v1
43
+
44
+ - **Obrolan** — aplikasi chatting gaya WhatsApp (daftar kontak, kirim pesan,
45
+ balasan otomatis). Riwayat tersimpan di `localStorage` perangkat.
46
+ - **Peramban** — mini browser berbasis iframe + kolom alamat + tautan cepat.
47
+ Catatan: banyak situs besar memblokir tampil di iframe (X-Frame-Options).
48
+ - **Catatan** — catatan teks tersimpan di `localStorage`.
49
+ - **Kalkulator** — operasi dasar tanpa `eval`.
50
+ - **Files** — penjelajah berkas virtual (folder, berkas teks, edit, hapus),
51
+ tersimpan di `localStorage`.
52
+ - **Pengaturan** — ganti wallpaper (4 pilihan) dan tema terang/gelap,
53
+ tersimpan di `localStorage` dan diterapkan saat desktop dibuka lagi.
54
+
55
+ ## Peta jalan (roadmap)
56
+
57
+ Urutan pembangunan berikutnya (satu item per jadwal build):
58
+
59
+ 1. Aplikasi Files (penjelajah berkas virtual + localStorage) — selesai 26 Okt 2026
60
+ 2. Aplikasi Pengaturan (ganti wallpaper, tema terang/gelap) — selesai 26 Okt 2026
61
+ 3. Notifikasi & pusat kontrol di taskbar
62
+ 4. Musik/video player mini
63
+ 5. Terminal web (perintah semu: help, about, echo, date)
64
+ 6. Mode jendela maximize + snap kiri/kanan
65
+ 7. Aplikasi Gambar (kanvas coret-coret + simpan)
66
+ 8. Widget jam/kalender di desktop
67
+ 9. Mode multi-bahasa (Indonesia/Inggris)
68
+ 10. Halaman login/pengguna + profil — selesai 26 Okt 2026 (dimajukan atas permintaan langsung)
69
+
70
+ > Nama proyek: **Rutbhics** (dipilih Kang Mas, 26 Okt 2026).
71
+
72
+ ## Struktur
73
+
74
+ ```
75
+ index.html — shell: wallpaper, ikon desktop, taskbar, menu Start
76
+ login.html — halaman login (akun lokal perangkat) + kunci desktop
77
+ css/style.css — seluruh tampilan desktop & aplikasi
78
+ js/desktop.js — window manager (drag, minimize, focus, taskbar, jam WIB, terapkan pengaturan tersimpan)
79
+ js/apps.js — aplikasi bawaan (obrolan, peramban, catatan, kalkulator, files, pengaturan)
80
+ package.json — metadata package npm + perintah `rutbhics`
81
+ bin/rutbhics.js — server statis lokal (tanpa dependensi) untuk perintah `rutbhics`
82
+ ```
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Rutbhics — server statis lokal, tanpa dependensi.
3
+ // Pakai: rutbhics [--port 8080] atau PORT=8080 rutbhics
4
+ // Lalu buka http://localhost:8080 di browser.
5
+ "use strict";
6
+ const http = require("node:http");
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+
10
+ const ROOT = path.join(__dirname, "..");
11
+ const MIME = {
12
+ ".html": "text/html; charset=utf-8",
13
+ ".css": "text/css; charset=utf-8",
14
+ ".js": "text/javascript; charset=utf-8",
15
+ ".json": "application/json; charset=utf-8",
16
+ ".png": "image/png",
17
+ ".jpg": "image/jpeg",
18
+ ".svg": "image/svg+xml",
19
+ ".ico": "image/x-icon",
20
+ ".txt": "text/plain; charset=utf-8"
21
+ };
22
+
23
+ function parsePort(argv) {
24
+ let port = parseInt(process.env.PORT, 10);
25
+ const i = argv.indexOf("--port");
26
+ if (i !== -1 && argv[i + 1]) port = parseInt(argv[i + 1], 10);
27
+ if (!port || port < 1 || port > 65535) port = 8080;
28
+ return port;
29
+ }
30
+
31
+ const port = parsePort(process.argv.slice(2));
32
+ const server = http.createServer((req, res) => {
33
+ try {
34
+ const url = new URL(req.url, "http://localhost");
35
+ let filePath = path.normalize(path.join(ROOT, url.pathname));
36
+ if (!filePath.startsWith(ROOT)) {
37
+ res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
38
+ res.end("Forbidden");
39
+ return;
40
+ }
41
+ if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) {
42
+ filePath = path.join(filePath, "index.html");
43
+ }
44
+ fs.readFile(filePath, (err, data) => {
45
+ if (err) {
46
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
47
+ res.end("Tidak ditemukan");
48
+ return;
49
+ }
50
+ const ext = path.extname(filePath).toLowerCase();
51
+ res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
52
+ res.end(data);
53
+ });
54
+ } catch (e) {
55
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
56
+ res.end("Terjadi kesalahan");
57
+ }
58
+ });
59
+ server.listen(port, () => {
60
+ console.log("Rutbhics berjalan di http://localhost:" + port);
61
+ console.log("Buka alamat itu di browser. Tekan Ctrl+C untuk berhenti.");
62
+ });
package/css/style.css ADDED
@@ -0,0 +1,252 @@
1
+ * { box-sizing: border-box; margin: 0; padding: 0; }
2
+ :root {
3
+ --bg: #0b1120;
4
+ --surface: rgba(19, 28, 49, .92);
5
+ --surface-solid: #131c31;
6
+ --ink: #e5eaf4;
7
+ --muted: #94a3b8;
8
+ --accent: #6366f1;
9
+ --accent-2: #22d3ee;
10
+ --border: rgba(255, 255, 255, .12);
11
+ --radius: 14px;
12
+ }
13
+ html, body { height: 100%; overflow: hidden; }
14
+ body {
15
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
16
+ color: var(--ink);
17
+ background: var(--bg);
18
+ user-select: none;
19
+ }
20
+ button { font: inherit; cursor: pointer; }
21
+ input, textarea { font: inherit; color: var(--ink); }
22
+
23
+ /* Wallpaper */
24
+ #wallpaper {
25
+ position: fixed; inset: 0; z-index: 0;
26
+ background:
27
+ radial-gradient(600px 400px at 20% 15%, rgba(99, 102, 241, .45), transparent 60%),
28
+ radial-gradient(700px 500px at 85% 80%, rgba(34, 211, 238, .30), transparent 60%),
29
+ linear-gradient(160deg, #0f172a, #1e1b4b 55%, #0b1120);
30
+ }
31
+ #wallpaper::after {
32
+ content: ""; position: absolute; inset: 0; opacity: .12;
33
+ background-image: radial-gradient(rgba(255,255,255,.7) 1px, transparent 1px);
34
+ background-size: 26px 26px;
35
+ }
36
+
37
+ /* Ikon desktop */
38
+ #desktop-icons {
39
+ position: fixed; z-index: 1; top: 16px; left: 16px;
40
+ display: flex; flex-direction: column; gap: 18px;
41
+ }
42
+ .desk-icon {
43
+ width: 76px; border: 0; background: transparent; color: var(--ink);
44
+ display: flex; flex-direction: column; align-items: center; gap: 6px;
45
+ padding: 8px 4px; border-radius: 10px;
46
+ }
47
+ .desk-icon:hover { background: rgba(255, 255, 255, .12); }
48
+ .desk-icon .emoji { font-size: 32px; line-height: 1; }
49
+ .desk-icon span { font-size: 12px; text-shadow: 0 1px 4px #000; }
50
+
51
+ /* Jendela */
52
+ .window {
53
+ position: fixed; z-index: 10; display: flex; flex-direction: column;
54
+ width: 640px; height: 440px; min-width: 320px; min-height: 240px;
55
+ background: var(--surface); border: 1px solid var(--border);
56
+ border-radius: var(--radius); box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
57
+ overflow: hidden; backdrop-filter: blur(8px);
58
+ }
59
+ .window.hidden-win { display: none; }
60
+ .win-header {
61
+ display: flex; align-items: center; gap: 10px;
62
+ padding: 10px 12px; background: rgba(255, 255, 255, .06);
63
+ cursor: grab; touch-action: none;
64
+ }
65
+ .win-header:active { cursor: grabbing; }
66
+ .win-title { flex: 1; font-weight: 600; font-size: 14px; }
67
+ .win-btn {
68
+ width: 30px; height: 28px; border: 0; border-radius: 8px;
69
+ background: rgba(255, 255, 255, .08); color: var(--ink); font-size: 14px;
70
+ }
71
+ .win-btn:hover { background: rgba(255, 255, 255, .2); }
72
+ .win-btn.close:hover { background: #ef4444; }
73
+ .win-body { flex: 1; overflow: auto; padding: 14px; user-select: text; }
74
+
75
+ /* Taskbar */
76
+ #taskbar {
77
+ position: fixed; z-index: 100; left: 0; right: 0; bottom: 0;
78
+ height: 52px; display: flex; align-items: center; gap: 8px;
79
+ padding: 6px 10px; background: rgba(11, 17, 32, .9);
80
+ border-top: 1px solid var(--border); backdrop-filter: blur(10px);
81
+ }
82
+ #start-btn {
83
+ border: 0; border-radius: 10px; padding: 8px 14px;
84
+ background: var(--accent); color: #fff; font-weight: 700;
85
+ }
86
+ #start-btn:hover { filter: brightness(1.15); }
87
+ #task-buttons { flex: 1; display: flex; gap: 6px; overflow-x: auto; }
88
+ .task-btn {
89
+ border: 1px solid transparent; border-radius: 10px; padding: 8px 12px;
90
+ background: transparent; color: var(--ink); white-space: nowrap; font-size: 13px;
91
+ }
92
+ .task-btn:hover { background: rgba(255, 255, 255, .1); }
93
+ .task-btn.active { background: rgba(255, 255, 255, .16); border-color: var(--border); }
94
+ #tray { display: flex; flex-direction: column; align-items: flex-end; font-size: 12px; line-height: 1.3; }
95
+ #clock-time { font-weight: 700; }
96
+ #clock-date { color: var(--muted); }
97
+
98
+ /* Menu Start */
99
+ #start-menu {
100
+ position: fixed; z-index: 99; left: 10px; bottom: 60px; width: 340px;
101
+ background: var(--surface); border: 1px solid var(--border);
102
+ border-radius: var(--radius); padding: 14px; box-shadow: 0 24px 60px rgba(0, 0, 0, .55);
103
+ }
104
+ #start-menu.hidden { display: none; }
105
+ #start-search {
106
+ width: 100%; padding: 10px 12px; border-radius: 10px;
107
+ border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none;
108
+ }
109
+ #start-search:focus { border-color: var(--accent); }
110
+ #start-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 12px; }
111
+ .start-app {
112
+ border: 0; border-radius: 12px; padding: 14px 6px;
113
+ background: transparent; color: var(--ink);
114
+ display: flex; flex-direction: column; gap: 6px; align-items: center; font-size: 12px;
115
+ }
116
+ .start-app:hover { background: rgba(255, 255, 255, .1); }
117
+ .start-app .emoji { font-size: 26px; }
118
+
119
+ /* Aplikasi: Chat */
120
+ .chat-wrap { display: flex; gap: 12px; height: 100%; min-height: 300px; }
121
+ .chat-contacts { width: 150px; display: flex; flex-direction: column; gap: 6px; overflow-y: auto; }
122
+ .chat-contact {
123
+ border: 1px solid transparent; border-radius: 10px; padding: 10px;
124
+ background: transparent; color: var(--ink); text-align: left; font-size: 13px;
125
+ }
126
+ .chat-contact.active { background: rgba(99, 102, 241, .25); border-color: var(--border); }
127
+ .chat-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
128
+ .chat-msgs { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 8px; padding: 4px; }
129
+ .msg { max-width: 80%; padding: 8px 12px; border-radius: 14px; font-size: 14px; line-height: 1.4; }
130
+ .msg.me { align-self: flex-end; background: var(--accent); color: #fff; border-bottom-right-radius: 4px; }
131
+ .msg.them { align-self: flex-start; background: rgba(255, 255, 255, .1); border-bottom-left-radius: 4px; }
132
+ .chat-input { display: flex; gap: 8px; margin-top: 10px; }
133
+ .chat-input input {
134
+ flex: 1; padding: 10px 12px; border-radius: 10px;
135
+ border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none;
136
+ }
137
+ .btn-primary {
138
+ border: 0; border-radius: 10px; padding: 10px 16px;
139
+ background: var(--accent); color: #fff; font-weight: 600;
140
+ }
141
+ .btn-primary:hover { filter: brightness(1.15); }
142
+
143
+ /* Aplikasi: Browser */
144
+ .browser-bar { display: flex; gap: 8px; margin-bottom: 10px; }
145
+ .browser-bar input {
146
+ flex: 1; padding: 10px 12px; border-radius: 10px;
147
+ border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none;
148
+ }
149
+ .browser-frame { width: 100%; height: 340px; border: 1px solid var(--border); border-radius: 10px; background: #fff; }
150
+ .quick-links { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
151
+ .chip {
152
+ border: 1px solid var(--border); border-radius: 999px; padding: 6px 12px;
153
+ background: transparent; color: var(--ink); font-size: 12px;
154
+ }
155
+ .chip:hover { background: rgba(255, 255, 255, .1); }
156
+ .hint { color: var(--muted); font-size: 12px; margin-top: 8px; line-height: 1.5; }
157
+
158
+ /* Aplikasi: Catatan & Kalkulator */
159
+ .notes-area {
160
+ width: 100%; height: 300px; padding: 12px; border-radius: 10px;
161
+ border: 1px solid var(--border); background: rgba(255, 255, 255, .06);
162
+ outline: none; resize: vertical; line-height: 1.5;
163
+ }
164
+ .calc-display {
165
+ width: 100%; padding: 14px; margin-bottom: 10px; text-align: right;
166
+ font-size: 26px; font-weight: 700; border-radius: 10px;
167
+ border: 1px solid var(--border); background: rgba(0, 0, 0, .3); min-height: 56px;
168
+ }
169
+ .calc-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; }
170
+ .calc-grid button {
171
+ padding: 14px 0; border-radius: 10px; border: 1px solid var(--border);
172
+ background: rgba(255, 255, 255, .07); color: var(--ink); font-size: 16px;
173
+ }
174
+ .calc-grid button:hover { background: rgba(255, 255, 255, .15); }
175
+ .calc-grid button.op { background: rgba(99, 102, 241, .3); }
176
+ .calc-grid button.eq { background: var(--accent); color: #fff; }
177
+
178
+ /* Aplikasi: Files */
179
+ .files-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; flex-wrap: wrap; }
180
+ .files-path { flex: 1; min-width: 120px; font-size: 13px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
181
+ .files-list { display: flex; flex-direction: column; gap: 4px; }
182
+ .file-row { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; }
183
+ .file-row:hover { background: rgba(255, 255, 255, .07); }
184
+ .file-icon { font-size: 20px; line-height: 1; }
185
+ .file-name { flex: 1; border: 0; background: transparent; color: var(--ink); text-align: left; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
186
+ .files-view { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 12px; }
187
+ .files-view-title { font-weight: 600; font-size: 14px; margin-bottom: 8px; }
188
+ .files-editor { width: 100%; height: 180px; padding: 12px; border-radius: 10px; border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none; resize: vertical; line-height: 1.5; margin-bottom: 10px; }
189
+
190
+ /* Halaman login & pengguna di menu Start */
191
+ .login-page { min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; position: relative; z-index: 1; }
192
+ .login-card { width: 100%; max-width: 360px; background: var(--surface); border: 1px solid var(--border); border-radius: 18px; padding: 28px; box-shadow: 0 24px 60px rgba(0, 0, 0, .55); }
193
+ .login-card h1 { font-size: 28px; margin-bottom: 4px; }
194
+ .login-card p.sub { color: var(--muted); font-size: 13px; margin-bottom: 20px; }
195
+ .login-card label { display: block; font-size: 13px; color: var(--muted); margin: 12px 0 6px; }
196
+ .login-card input { width: 100%; padding: 11px 12px; border-radius: 10px; border: 1px solid var(--border); background: rgba(255, 255, 255, .06); outline: none; }
197
+ .login-card input:focus { border-color: var(--accent); }
198
+ .login-card .btn-primary { width: 100%; margin-top: 18px; }
199
+ .login-error { color: #f87171; font-size: 13px; min-height: 18px; margin-top: 10px; }
200
+ .login-link { background: none; border: 0; color: var(--accent-2); font-size: 13px; margin-top: 14px; }
201
+ .login-link.hidden { display: none; }
202
+ #start-user { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); font-size: 12px; color: var(--muted); }
203
+ #logout-btn { border: 1px solid var(--border); border-radius: 8px; padding: 6px 12px; background: transparent; color: var(--ink); font-size: 12px; }
204
+ #logout-btn:hover { background: rgba(255, 255, 255, .1); }
205
+
206
+ /* Aplikasi: Pengaturan */
207
+ .settings-title { font-size: 16px; margin-bottom: 4px; }
208
+ .settings-head { font-size: 13px; color: var(--muted); margin: 14px 0 8px; }
209
+ .wp-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
210
+ .wp-swatch {
211
+ position: relative; height: 72px; border: 2px solid transparent;
212
+ border-radius: 12px; overflow: hidden; padding: 0;
213
+ }
214
+ .wp-swatch:hover { filter: brightness(1.1); }
215
+ .wp-swatch.active { border-color: var(--accent-2); box-shadow: 0 0 0 2px rgba(34, 211, 238, .35); }
216
+ .wp-label {
217
+ position: absolute; left: 8px; bottom: 6px; font-size: 12px; color: #fff;
218
+ text-shadow: 0 1px 4px #000;
219
+ }
220
+ .theme-row { display: flex; gap: 8px; }
221
+ .theme-btn.active { background: rgba(99, 102, 241, .3); border-color: var(--accent); }
222
+
223
+ /* Tema terang */
224
+ body.light-theme {
225
+ --bg: #e2e8f0;
226
+ --surface: rgba(255, 255, 255, .94);
227
+ --surface-solid: #ffffff;
228
+ --ink: #0f172a;
229
+ --muted: #475569;
230
+ --border: rgba(15, 23, 42, .15);
231
+ }
232
+ body.light-theme #taskbar { background: rgba(255, 255, 255, .92); }
233
+ body.light-theme .win-header { background: rgba(15, 23, 42, .05); }
234
+ body.light-theme .desk-icon:hover,
235
+ body.light-theme .task-btn:hover,
236
+ body.light-theme .start-app:hover,
237
+ body.light-theme .file-row:hover,
238
+ body.light-theme .chip:hover,
239
+ body.light-theme .win-btn:hover { background: rgba(15, 23, 42, .08); }
240
+ body.light-theme .desk-icon span { text-shadow: 0 1px 4px rgba(255, 255, 255, .8); }
241
+ body.light-theme .chat-input input,
242
+ body.light-theme .browser-bar input,
243
+ body.light-theme #start-search,
244
+ body.light-theme .notes-area,
245
+ body.light-theme .files-editor,
246
+ body.light-theme .login-card input { background: rgba(15, 23, 42, .05); }
247
+ body.light-theme .msg.them { background: rgba(15, 23, 42, .08); }
248
+ body.light-theme .calc-display { background: rgba(15, 23, 42, .06); }
249
+ body.light-theme .calc-grid button { background: rgba(15, 23, 42, .06); }
250
+ body.light-theme .calc-grid button:hover { background: rgba(15, 23, 42, .12); }
251
+ body.light-theme .chat-contact.active { background: rgba(99, 102, 241, .15); }
252
+ body.light-theme #wallpaper::after { opacity: .06; }
package/index.html ADDED
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Rutbhics</title>
7
+ <link rel="stylesheet" href="css/style.css">
8
+ <script>
9
+ // Kunci desktop: tanpa sesi login, kembali ke halaman login.
10
+ try {
11
+ if (!sessionStorage.getItem("wd-session")) { location.replace("login.html"); }
12
+ } catch (e) { /* abaikan: kunci demo, bukan pengaman sungguhan */ }
13
+ </script>
14
+ </head>
15
+ <body>
16
+ <div id="wallpaper"></div>
17
+
18
+ <!-- Ikon desktop -->
19
+ <div id="desktop-icons"></div>
20
+
21
+ <!-- Jendela -->
22
+ <div id="windows"></div>
23
+
24
+ <!-- Menu Start -->
25
+ <div id="start-menu" class="hidden">
26
+ <input id="start-search" type="text" placeholder="Cari aplikasi...">
27
+ <div id="start-grid"></div>
28
+ <div id="start-user">
29
+ <span id="start-username"></span>
30
+ <button id="logout-btn">Keluar</button>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Taskbar -->
35
+ <footer id="taskbar">
36
+ <button id="start-btn" title="Mulai">⊞ <span>Mulai</span></button>
37
+ <div id="task-buttons"></div>
38
+ <div id="tray">
39
+ <span id="clock-time">--:--</span>
40
+ <span id="clock-date">--</span>
41
+ </div>
42
+ </footer>
43
+
44
+ <script src="js/desktop.js"></script>
45
+ <script src="js/apps.js"></script>
46
+ </body>
47
+ </html>
package/js/apps.js ADDED
@@ -0,0 +1,364 @@
1
+ // Web Desktop — aplikasi bawaan: Obrolan, Peramban, Catatan, Kalkulator, Files, Pengaturan.
2
+ // Data Obrolan, Catatan & Files tersimpan di localStorage perangkat yang membuka web ini.
3
+ (function () {
4
+ "use strict";
5
+
6
+ function store(key, fallback) {
7
+ try {
8
+ var raw = localStorage.getItem(key);
9
+ return raw ? JSON.parse(raw) : fallback;
10
+ } catch (e) { return fallback; }
11
+ }
12
+ function save(key, val) {
13
+ try { localStorage.setItem(key, JSON.stringify(val)); } catch (e) { /* abaikan */ }
14
+ }
15
+ function el(html) {
16
+ var t = document.createElement("div");
17
+ t.innerHTML = html;
18
+ return t.firstElementChild;
19
+ }
20
+
21
+ // ---------- Obrolan (gaya WhatsApp, demo lokal) ----------
22
+ var CONTACTS = [
23
+ { id: "teman", name: "Teman", hello: "Hai! Lagi ngapain?" },
24
+ { id: "grup", name: "Grup Keluarga", hello: "Selamat datang di grup keluarga!" },
25
+ { id: "bot", name: "Bot Asisten", hello: "Halo! Aku bot demo. Tulis pesan, nanti aku balas." }
26
+ ];
27
+ var REPLIES = ["Siap, noted!", "Oke, lanjut!", "Haha, bener juga.", "Menarik, ceritain lagi dong.", "Otw!"];
28
+ function renderChat(body) {
29
+ var data = store("wd-chat", {});
30
+ CONTACTS.forEach(function (c) { if (!data[c.id]) data[c.id] = [{ from: "them", text: c.hello }]; });
31
+ var active = CONTACTS[0].id;
32
+ var wrap = el('<div class="chat-wrap"></div>');
33
+ var listEl = el('<div class="chat-contacts"></div>');
34
+ var mainEl = el('<div class="chat-main"></div>');
35
+ var msgsEl = el('<div class="chat-msgs"></div>');
36
+ var inputRow = el('<div class="chat-input"></div>');
37
+ var input = el('<input type="text" placeholder="Tulis pesan...">');
38
+ var send = el('<button class="btn-primary">Kirim</button>');
39
+ inputRow.appendChild(input); inputRow.appendChild(send);
40
+ mainEl.appendChild(msgsEl); mainEl.appendChild(inputRow);
41
+ wrap.appendChild(listEl); wrap.appendChild(mainEl);
42
+ body.appendChild(wrap);
43
+
44
+ function drawContacts() {
45
+ listEl.innerHTML = "";
46
+ CONTACTS.forEach(function (c) {
47
+ var b = el('<button class="chat-contact' + (c.id === active ? " active" : "") + '">' + c.name + "</button>");
48
+ b.addEventListener("click", function () { active = c.id; drawContacts(); drawMsgs(); });
49
+ listEl.appendChild(b);
50
+ });
51
+ }
52
+ function drawMsgs() {
53
+ msgsEl.innerHTML = "";
54
+ data[active].forEach(function (m) {
55
+ var d = el('<div class="msg ' + (m.from === "me" ? "me" : "them") + '"></div>');
56
+ d.textContent = m.text;
57
+ msgsEl.appendChild(d);
58
+ });
59
+ msgsEl.scrollTop = msgsEl.scrollHeight;
60
+ }
61
+ function sendMsg() {
62
+ var text = input.value.trim();
63
+ if (!text) return;
64
+ data[active].push({ from: "me", text: text });
65
+ input.value = "";
66
+ save("wd-chat", data);
67
+ drawMsgs();
68
+ var reply = REPLIES[Math.floor(Math.random() * REPLIES.length)];
69
+ setTimeout(function () {
70
+ data[active].push({ from: "them", text: reply });
71
+ save("wd-chat", data);
72
+ drawMsgs();
73
+ }, 900);
74
+ }
75
+ send.addEventListener("click", sendMsg);
76
+ input.addEventListener("keydown", function (e) { if (e.key === "Enter") sendMsg(); });
77
+ drawContacts();
78
+ drawMsgs();
79
+ }
80
+
81
+ // ---------- Peramban ----------
82
+ function renderBrowser(body) {
83
+ var bar = el('<div class="browser-bar"></div>');
84
+ var input = el('<input type="text" placeholder="Ketik alamat situs, mis. wikipedia.org">');
85
+ var go = el('<button class="btn-primary">Kunjungi</button>');
86
+ bar.appendChild(input); bar.appendChild(go);
87
+ var links = el('<div class="quick-links"></div>');
88
+ ["wikipedia.org", "openstreetmap.org", "example.com"].forEach(function (u) {
89
+ var c = el('<button class="chip">' + u + "</button>");
90
+ c.addEventListener("click", function () { openUrl(u); });
91
+ links.appendChild(c);
92
+ });
93
+ var frame = el('<iframe class="browser-frame" sandbox="allow-scripts allow-same-origin allow-forms"></iframe>');
94
+ var hint = el('<p class="hint">Catatan: banyak situs besar (Google, YouTube, dll.) memblokir tampil di dalam frame, jadi halaman bisa tampak kosong. Itu batasan teknis browser, bukan bug aplikasi ini.</p>');
95
+ body.appendChild(bar); body.appendChild(links); body.appendChild(frame); body.appendChild(hint);
96
+ function openUrl(raw) {
97
+ var u = raw.trim();
98
+ if (!u) return;
99
+ if (/\s/.test(u) || u.indexOf(".") === -1) {
100
+ frame.src = "https://duckduckgo.com/?q=" + encodeURIComponent(u);
101
+ } else {
102
+ frame.src = /^https?:\/\//i.test(u) ? u : "https://" + u;
103
+ }
104
+ input.value = frame.src;
105
+ }
106
+ go.addEventListener("click", function () { openUrl(input.value); });
107
+ input.addEventListener("keydown", function (e) { if (e.key === "Enter") openUrl(input.value); });
108
+ frame.src = "https://example.com";
109
+ }
110
+
111
+ // ---------- Catatan ----------
112
+ function renderNotes(body) {
113
+ var area = el('<textarea class="notes-area" placeholder="Tulis catatanmu di sini..."></textarea>');
114
+ area.value = store("wd-notes", "");
115
+ var btn = el('<button class="btn-primary" style="margin-top:10px">Simpan</button>');
116
+ var ok = el('<span class="hint" style="margin-left:10px"></span>');
117
+ btn.addEventListener("click", function () {
118
+ save("wd-notes", area.value);
119
+ ok.textContent = "Tersimpan ✓";
120
+ setTimeout(function () { ok.textContent = ""; }, 2000);
121
+ });
122
+ body.appendChild(area); body.appendChild(btn); body.appendChild(ok);
123
+ }
124
+
125
+ // ---------- Kalkulator ----------
126
+ function renderCalc(body) {
127
+ var disp = el('<div class="calc-display">0</div>');
128
+ var grid = el('<div class="calc-grid"></div>');
129
+ var cur = "0", prev = null, op = null, fresh = true;
130
+ function show(v) { disp.textContent = v; }
131
+ function compute() {
132
+ var a = parseFloat(prev), b = parseFloat(cur);
133
+ if (op === "+") return a + b;
134
+ if (op === "-") return a - b;
135
+ if (op === "×") return a * b;
136
+ if (op === "÷") return b === 0 ? "Error" : a / b;
137
+ return b;
138
+ }
139
+ function press(k) {
140
+ if (/^[0-9.]$/.test(k)) {
141
+ if (fresh) { cur = k === "." ? "0." : k; fresh = false; }
142
+ else if (!(k === "." && cur.indexOf(".") !== -1)) { cur = cur === "0" && k !== "." ? k : cur + k; }
143
+ show(cur);
144
+ } else if (k === "C") {
145
+ cur = "0"; prev = null; op = null; fresh = true; show(cur);
146
+ } else if (k === "=") {
147
+ if (op !== null) { cur = String(compute()); prev = null; op = null; fresh = true; show(cur); }
148
+ } else {
149
+ if (op !== null && !fresh) { cur = String(compute()); }
150
+ prev = cur; op = k; fresh = true; show(cur);
151
+ }
152
+ }
153
+ ["7", "8", "9", "÷", "4", "5", "6", "×", "1", "2", "3", "-", "0", ".", "C", "+", "="].forEach(function (k) {
154
+ var b = el("<button>" + k + "</button>");
155
+ if (["÷", "×", "-", "+"].indexOf(k) !== -1) b.className = "op";
156
+ if (k === "=") b.className = "eq";
157
+ b.addEventListener("click", function () { press(k); });
158
+ grid.appendChild(b);
159
+ });
160
+ body.appendChild(disp); body.appendChild(grid);
161
+ }
162
+
163
+ // ---------- Files (penjelajah berkas virtual, data lokal) ----------
164
+ // Struktur: { name, type: "folder"|"file", children: [], content: "" }
165
+ function defaultFS() {
166
+ return {
167
+ name: "", type: "folder", children: [
168
+ { name: "Dokumen", type: "folder", children: [
169
+ { name: "Selamat datang.txt", type: "file",
170
+ content: "Selamat datang di Files Rutbhics!\n\nIni penjelajah berkas virtual: folder dan berkas teks yang kamu buat tersimpan di perangkat ini (localStorage), jadi tetap ada saat halaman dibuka lagi.\n\nCoba: buat folder baru, buat berkas baru, buka berkas untuk mengedit isinya, atau hapus yang tidak perlu." },
171
+ { name: "Tentang Rutbhics.txt", type: "file",
172
+ content: "Rutbhics adalah aplikasi desktop di dalam browser.\nDibangun dengan HTML, CSS, dan JavaScript murni — tanpa framework." }
173
+ ]},
174
+ { name: "Gambar", type: "folder", children: [] },
175
+ { name: "Musik", type: "folder", children: [] }
176
+ ]
177
+ };
178
+ }
179
+ function renderFiles(body) {
180
+ var root = store("wd-files", null) || defaultFS();
181
+ if (!root.children) root = defaultFS();
182
+ save("wd-files", root);
183
+ var path = []; // array nama folder dari root
184
+ var bar = el('<div class="files-bar"></div>');
185
+ var pathEl = el('<span class="files-path">/</span>');
186
+ var upBtn = el('<button class="chip">↑ Naik</button>');
187
+ var newFolderBtn = el('<button class="btn-primary">Folder Baru</button>');
188
+ var newFileBtn = el('<button class="btn-primary">Berkas Baru</button>');
189
+ bar.appendChild(pathEl); bar.appendChild(upBtn);
190
+ bar.appendChild(newFolderBtn); bar.appendChild(newFileBtn);
191
+ var listEl = el('<div class="files-list"></div>');
192
+ var viewEl = el('<div class="files-view"></div>');
193
+ body.appendChild(bar); body.appendChild(listEl); body.appendChild(viewEl);
194
+
195
+ function currentFolder() {
196
+ var node = root;
197
+ path.forEach(function (name) {
198
+ var next = null;
199
+ node.children.forEach(function (c) {
200
+ if (c.type === "folder" && c.name === name) next = c;
201
+ });
202
+ if (next) node = next;
203
+ });
204
+ return node;
205
+ }
206
+ function persist() { save("wd-files", root); }
207
+ function uniqueName(base, folder) {
208
+ var name = base, i = 2;
209
+ function exists(n) {
210
+ return folder.children.some(function (c) { return c.name === n; });
211
+ }
212
+ while (exists(name)) { name = base + " (" + i + ")"; i += 1; }
213
+ return name;
214
+ }
215
+ function draw() {
216
+ var folder = currentFolder();
217
+ pathEl.textContent = "/" + path.join("/");
218
+ listEl.innerHTML = "";
219
+ viewEl.innerHTML = "";
220
+ if (!folder.children.length) {
221
+ var empty = el('<p class="hint">Folder ini kosong. Buat folder atau berkas baru dari tombol di atas.</p>');
222
+ listEl.appendChild(empty);
223
+ return;
224
+ }
225
+ folder.children.forEach(function (item) {
226
+ var row = el('<div class="file-row"></div>');
227
+ var icon = el('<span class="file-icon"></span>');
228
+ icon.textContent = item.type === "folder" ? "📁" : "📄";
229
+ var nameBtn = el('<button class="file-name"></button>');
230
+ nameBtn.textContent = item.name;
231
+ nameBtn.addEventListener("click", function () { openItem(item); });
232
+ var del = el('<button class="chip">Hapus</button>');
233
+ del.addEventListener("click", function () {
234
+ folder.children = folder.children.filter(function (c) { return c !== item; });
235
+ persist(); draw();
236
+ });
237
+ row.appendChild(icon); row.appendChild(nameBtn); row.appendChild(del);
238
+ listEl.appendChild(row);
239
+ });
240
+ }
241
+ function openItem(item) {
242
+ if (item.type === "folder") { path.push(item.name); draw(); return; }
243
+ viewEl.innerHTML = "";
244
+ var title = el('<p class="files-view-title"></p>');
245
+ title.textContent = item.name;
246
+ var area = el('<textarea class="files-editor"></textarea>');
247
+ area.value = item.content || "";
248
+ var saveBtn = el('<button class="btn-primary">Simpan</button>');
249
+ var ok = el('<span class="hint" style="margin-left:10px"></span>');
250
+ saveBtn.addEventListener("click", function () {
251
+ item.content = area.value; persist();
252
+ ok.textContent = "Tersimpan ✓";
253
+ setTimeout(function () { ok.textContent = ""; }, 2000);
254
+ });
255
+ viewEl.appendChild(title); viewEl.appendChild(area);
256
+ viewEl.appendChild(saveBtn); viewEl.appendChild(ok);
257
+ }
258
+ upBtn.addEventListener("click", function () {
259
+ if (path.length) { path.pop(); draw(); }
260
+ });
261
+ newFolderBtn.addEventListener("click", function () {
262
+ var folder = currentFolder();
263
+ var name = uniqueName("Folder baru", folder);
264
+ folder.children.push({ name: name, type: "folder", children: [] });
265
+ persist(); draw();
266
+ });
267
+ newFileBtn.addEventListener("click", function () {
268
+ var folder = currentFolder();
269
+ var name = uniqueName("Berkas baru.txt", folder);
270
+ folder.children.push({ name: name, type: "file", content: "" });
271
+ persist(); draw();
272
+ });
273
+ draw();
274
+ }
275
+
276
+ // ---------- Pengaturan (wallpaper & tema, tersimpan lokal) ----------
277
+ // Kunci wd-wallpaper / wd-theme disimpan sebagai string mentah (bukan JSON),
278
+ // supaya desktop.js bisa membacanya langsung dengan getItem.
279
+ var WALLPAPERS = {
280
+ malam: { name: "Ungu Malam",
281
+ bg: "radial-gradient(600px 400px at 20% 15%, rgba(99, 102, 241, .45), transparent 60%), radial-gradient(700px 500px at 85% 80%, rgba(34, 211, 238, .30), transparent 60%), linear-gradient(160deg, #0f172a, #1e1b4b 55%, #0b1120)" },
282
+ senja: { name: "Senja Jingga",
283
+ bg: "radial-gradient(600px 400px at 20% 15%, rgba(251, 146, 60, .45), transparent 60%), radial-gradient(700px 500px at 85% 80%, rgba(244, 63, 94, .30), transparent 60%), linear-gradient(160deg, #431407, #7c2d12 55%, #1c1917)" },
284
+ hutan: { name: "Hutan Hijau",
285
+ bg: "radial-gradient(600px 400px at 20% 15%, rgba(52, 211, 153, .40), transparent 60%), radial-gradient(700px 500px at 85% 80%, rgba(163, 230, 53, .25), transparent 60%), linear-gradient(160deg, #052e16, #14532d 55%, #022c22)" },
286
+ laut: { name: "Laut Biru",
287
+ bg: "radial-gradient(600px 400px at 20% 15%, rgba(56, 189, 248, .45), transparent 60%), radial-gradient(700px 500px at 85% 80%, rgba(45, 212, 191, .30), transparent 60%), linear-gradient(160deg, #082f49, #0c4a6e 55%, #020617)" }
288
+ };
289
+ window.WALLPAPERS = WALLPAPERS;
290
+ function applyWallpaper(id) {
291
+ var w = document.getElementById("wallpaper");
292
+ var p = WALLPAPERS[id];
293
+ if (w && p) w.style.background = p.bg;
294
+ }
295
+ function applyTheme(theme) {
296
+ document.body.classList.toggle("light-theme", theme === "light");
297
+ }
298
+ function renderSettings(body) {
299
+ var curWp = "malam", curTheme = "dark";
300
+ try {
301
+ curWp = localStorage.getItem("wd-wallpaper") || "malam";
302
+ curTheme = localStorage.getItem("wd-theme") || "dark";
303
+ } catch (e) {}
304
+ if (!WALLPAPERS[curWp]) curWp = "malam";
305
+
306
+ body.appendChild(el('<h3 class="settings-title">Personalisasi desktop</h3>'));
307
+ body.appendChild(el('<p class="settings-head">Wallpaper</p>'));
308
+ var grid = el('<div class="wp-grid"></div>');
309
+ Object.keys(WALLPAPERS).forEach(function (id) {
310
+ var b = el('<button class="wp-swatch' + (id === curWp ? " active" : "") + '"></button>');
311
+ b.style.background = WALLPAPERS[id].bg;
312
+ b.title = WALLPAPERS[id].name;
313
+ var lbl = el('<span class="wp-label"></span>');
314
+ lbl.textContent = WALLPAPERS[id].name;
315
+ b.appendChild(lbl);
316
+ b.addEventListener("click", function () {
317
+ curWp = id;
318
+ try { localStorage.setItem("wd-wallpaper", id); } catch (e) {}
319
+ applyWallpaper(id);
320
+ grid.querySelectorAll(".wp-swatch").forEach(function (s) { s.classList.remove("active"); });
321
+ b.classList.add("active");
322
+ });
323
+ grid.appendChild(b);
324
+ });
325
+ body.appendChild(grid);
326
+
327
+ body.appendChild(el('<p class="settings-head">Tema</p>'));
328
+ var row = el('<div class="theme-row"></div>');
329
+ var darkBtn = el('<button class="chip theme-btn' + (curTheme === "dark" ? " active" : "") + '">🌙 Gelap</button>');
330
+ var lightBtn = el('<button class="chip theme-btn' + (curTheme === "light" ? " active" : "") + '">☀️ Terang</button>');
331
+ function setTheme(t) {
332
+ curTheme = t;
333
+ try { localStorage.setItem("wd-theme", t); } catch (e) {}
334
+ applyTheme(t);
335
+ darkBtn.classList.toggle("active", t === "dark");
336
+ lightBtn.classList.toggle("active", t === "light");
337
+ }
338
+ darkBtn.addEventListener("click", function () { setTheme("dark"); });
339
+ lightBtn.addEventListener("click", function () { setTheme("light"); });
340
+ row.appendChild(darkBtn); row.appendChild(lightBtn);
341
+ body.appendChild(row);
342
+
343
+ var reset = el('<button class="btn-primary" style="margin-top:18px">Kembalikan bawaan</button>');
344
+ reset.addEventListener("click", function () {
345
+ try { localStorage.removeItem("wd-wallpaper"); localStorage.removeItem("wd-theme"); } catch (e) {}
346
+ applyWallpaper("malam");
347
+ setTheme("dark");
348
+ var sw = grid.querySelectorAll(".wp-swatch");
349
+ sw.forEach(function (s) { s.classList.remove("active"); });
350
+ if (sw[0]) sw[0].classList.add("active");
351
+ });
352
+ body.appendChild(reset);
353
+ body.appendChild(el('<p class="hint">Pengaturan tersimpan di perangkat ini dan langsung diterapkan saat desktop dibuka lagi.</p>'));
354
+ }
355
+
356
+ window.APPS = {
357
+ chat: { emoji: "💬", title: "Obrolan", render: renderChat },
358
+ browser: { emoji: "🌐", title: "Peramban", render: renderBrowser },
359
+ notes: { emoji: "📝", title: "Catatan", render: renderNotes },
360
+ calc: { emoji: "🧮", title: "Kalkulator", render: renderCalc },
361
+ files: { emoji: "📁", title: "Files", render: renderFiles },
362
+ settings: { emoji: "⚙️", title: "Pengaturan", render: renderSettings }
363
+ };
364
+ })();
package/js/desktop.js ADDED
@@ -0,0 +1,185 @@
1
+ // Web Desktop — window manager, taskbar, start menu, jam (WIB).
2
+ // Semua aplikasi terdaftar di APPS (lihat js/apps.js untuk isi tiap aplikasi).
3
+ (function () {
4
+ "use strict";
5
+
6
+ var APPS = window.APPS || {};
7
+ var zTop = 10;
8
+ var winCount = 0;
9
+ var openWins = {}; // appId -> elemen window
10
+
11
+ var windowsEl = document.getElementById("windows");
12
+ var iconsEl = document.getElementById("desktop-icons");
13
+ var taskEl = document.getElementById("task-buttons");
14
+ var startMenu = document.getElementById("start-menu");
15
+ var startGrid = document.getElementById("start-grid");
16
+ var startSearch = document.getElementById("start-search");
17
+
18
+ function appList() {
19
+ return Object.keys(APPS).map(function (id) { return { id: id, def: APPS[id] }; });
20
+ }
21
+
22
+ function focusWin(id) {
23
+ var w = openWins[id];
24
+ if (!w) return;
25
+ zTop += 1;
26
+ w.style.zIndex = zTop;
27
+ document.querySelectorAll(".task-btn").forEach(function (b) {
28
+ b.classList.toggle("active", b.dataset.app === id && !w.classList.contains("hidden-win"));
29
+ });
30
+ }
31
+
32
+ function renderTaskbar() {
33
+ taskEl.innerHTML = "";
34
+ Object.keys(openWins).forEach(function (id) {
35
+ var b = document.createElement("button");
36
+ b.className = "task-btn";
37
+ b.dataset.app = id;
38
+ b.textContent = APPS[id].emoji + " " + APPS[id].title;
39
+ b.addEventListener("click", function () {
40
+ var w = openWins[id];
41
+ if (w.classList.contains("hidden-win")) {
42
+ w.classList.remove("hidden-win");
43
+ focusWin(id);
44
+ } else if (parseInt(w.style.zIndex, 10) === zTop) {
45
+ w.classList.add("hidden-win");
46
+ b.classList.remove("active");
47
+ } else {
48
+ focusWin(id);
49
+ }
50
+ });
51
+ taskEl.appendChild(b);
52
+ });
53
+ }
54
+
55
+ function openApp(id) {
56
+ var def = APPS[id];
57
+ if (!def) return;
58
+ if (openWins[id]) {
59
+ openWins[id].classList.remove("hidden-win");
60
+ focusWin(id);
61
+ return;
62
+ }
63
+ var w = document.createElement("section");
64
+ w.className = "window";
65
+ w.style.left = (60 + (winCount % 5) * 36) + "px";
66
+ w.style.top = (40 + (winCount % 5) * 30) + "px";
67
+ winCount += 1;
68
+ w.innerHTML =
69
+ '<div class="win-header"><span>' + def.emoji + '</span>' +
70
+ '<span class="win-title">' + def.title + '</span>' +
71
+ '<button class="win-btn" data-act="min" title="Minimize">—</button>' +
72
+ '<button class="win-btn close" data-act="close" title="Tutup">✕</button></div>' +
73
+ '<div class="win-body"></div>';
74
+ var body = w.querySelector(".win-body");
75
+ def.render(body);
76
+ w.querySelector('[data-act="min"]').addEventListener("click", function () {
77
+ w.classList.add("hidden-win");
78
+ renderTaskbar();
79
+ });
80
+ w.querySelector('[data-act="close"]').addEventListener("click", function () {
81
+ w.remove();
82
+ delete openWins[id];
83
+ renderTaskbar();
84
+ });
85
+ w.addEventListener("pointerdown", function () { focusWin(id); });
86
+ makeDraggable(w, w.querySelector(".win-header"));
87
+ windowsEl.appendChild(w);
88
+ openWins[id] = w;
89
+ focusWin(id);
90
+ renderTaskbar();
91
+ startMenu.classList.add("hidden");
92
+ }
93
+
94
+ function makeDraggable(win, handle) {
95
+ var dx = 0, dy = 0, dragging = false;
96
+ handle.addEventListener("pointerdown", function (e) {
97
+ if (e.target.closest(".win-btn")) return;
98
+ dragging = true;
99
+ dx = e.clientX - win.offsetLeft;
100
+ dy = e.clientY - win.offsetTop;
101
+ handle.setPointerCapture(e.pointerId);
102
+ });
103
+ handle.addEventListener("pointermove", function (e) {
104
+ if (!dragging) return;
105
+ var x = Math.max(0, Math.min(e.clientX - dx, window.innerWidth - 120));
106
+ var y = Math.max(0, Math.min(e.clientY - dy, window.innerHeight - 100));
107
+ win.style.left = x + "px";
108
+ win.style.top = y + "px";
109
+ });
110
+ handle.addEventListener("pointerup", function () { dragging = false; });
111
+ }
112
+
113
+ // Ikon desktop + menu Start
114
+ function renderLaunchers(filter) {
115
+ var q = (filter || "").toLowerCase();
116
+ iconsEl.innerHTML = "";
117
+ startGrid.innerHTML = "";
118
+ appList().forEach(function (a) {
119
+ if (q && a.def.title.toLowerCase().indexOf(q) === -1) return;
120
+ var icon = document.createElement("button");
121
+ icon.className = "desk-icon";
122
+ icon.innerHTML = '<span class="emoji">' + a.def.emoji + '</span><span>' + a.def.title + '</span>';
123
+ icon.addEventListener("click", function () { openApp(a.id); });
124
+ iconsEl.appendChild(icon);
125
+ var s = document.createElement("button");
126
+ s.className = "start-app";
127
+ s.innerHTML = '<span class="emoji">' + a.def.emoji + '</span><span>' + a.def.title + '</span>';
128
+ s.addEventListener("click", function () { openApp(a.id); });
129
+ startGrid.appendChild(s);
130
+ });
131
+ }
132
+
133
+ document.getElementById("start-btn").addEventListener("click", function () {
134
+ startMenu.classList.toggle("hidden");
135
+ if (!startMenu.classList.contains("hidden")) startSearch.focus();
136
+ });
137
+ startSearch.addEventListener("input", function () { renderLaunchers(startSearch.value); });
138
+ document.addEventListener("click", function (e) {
139
+ if (!startMenu.classList.contains("hidden") &&
140
+ !e.target.closest("#start-menu") && !e.target.closest("#start-btn")) {
141
+ startMenu.classList.add("hidden");
142
+ }
143
+ });
144
+
145
+ // Jam WIB (Asia/Jakarta) — live tiap detik
146
+ var timeFmt = new Intl.DateTimeFormat("id-ID", {
147
+ timeZone: "Asia/Jakarta", hour: "2-digit", minute: "2-digit", hour12: false
148
+ });
149
+ var dateFmt = new Intl.DateTimeFormat("id-ID", {
150
+ timeZone: "Asia/Jakarta", day: "numeric", month: "short", year: "numeric"
151
+ });
152
+ function tick() {
153
+ var now = new Date();
154
+ document.getElementById("clock-time").textContent = timeFmt.format(now);
155
+ document.getElementById("clock-date").textContent = dateFmt.format(now);
156
+ }
157
+ tick();
158
+ setInterval(tick, 1000);
159
+
160
+ // Sesi login: tampilkan pengguna & tombol keluar di menu Start.
161
+ try {
162
+ var sessUser = sessionStorage.getItem("wd-session");
163
+ var nameEl = document.getElementById("start-username");
164
+ var logoutBtn = document.getElementById("logout-btn");
165
+ if (nameEl) nameEl.textContent = sessUser ? "Masuk sebagai " + sessUser : "";
166
+ if (logoutBtn) logoutBtn.addEventListener("click", function () {
167
+ try { sessionStorage.removeItem("wd-session"); } catch (e) {}
168
+ location.href = "login.html";
169
+ });
170
+ } catch (e) {}
171
+
172
+ // Pengaturan tersimpan (diatur lewat aplikasi Pengaturan): wallpaper & tema.
173
+ try {
174
+ var wallEl = document.getElementById("wallpaper");
175
+ var savedWp = localStorage.getItem("wd-wallpaper");
176
+ if (wallEl && savedWp && window.WALLPAPERS && window.WALLPAPERS[savedWp]) {
177
+ wallEl.style.background = window.WALLPAPERS[savedWp].bg;
178
+ }
179
+ if (localStorage.getItem("wd-theme") === "light") {
180
+ document.body.classList.add("light-theme");
181
+ }
182
+ } catch (e) {}
183
+
184
+ renderLaunchers("");
185
+ })();
package/login.html ADDED
@@ -0,0 +1,101 @@
1
+ <!DOCTYPE html>
2
+ <html lang="id">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Masuk — Rutbhics</title>
7
+ <link rel="stylesheet" href="css/style.css">
8
+ </head>
9
+ <body>
10
+ <div id="wallpaper"></div>
11
+ <main class="login-page">
12
+ <section class="login-card">
13
+ <h1>🖥️ Rutbhics</h1>
14
+ <p class="sub">Masuk ke desktop.</p>
15
+ <form id="login-form">
16
+ <label for="username">Nama pengguna</label>
17
+ <input id="username" type="text" autocomplete="username" required>
18
+ <label for="password">Kata sandi</label>
19
+ <input id="password" type="password" autocomplete="current-password" required>
20
+ <p id="login-error" class="login-error"></p>
21
+ <button id="login-btn" class="btn-primary" type="submit">Masuk</button>
22
+ </form>
23
+ <button id="reset-account" class="login-link hidden">Ganti akun perangkat (hapus akun ini)</button>
24
+ <p class="hint">Akun tersimpan lokal di perangkat ini (demo). Ini bukan autentikasi server sungguhan — siapa pun yang memegang berkasnya bisa melewatinya. Untuk autentikasi multi-pengguna sungguhan, perlu backend di tahap berikutnya.</p>
25
+ </section>
26
+ </main>
27
+ <script>
28
+ (function () {
29
+ "use strict";
30
+ var USER_KEY = "wd-user"; // localStorage: { username, passHash }
31
+ var SESSION_KEY = "wd-session"; // sessionStorage: username
32
+ var form = document.getElementById("login-form");
33
+ var userEl = document.getElementById("username");
34
+ var passEl = document.getElementById("password");
35
+ var errEl = document.getElementById("login-error");
36
+ var btn = document.getElementById("login-btn");
37
+ var resetBtn = document.getElementById("reset-account");
38
+
39
+ function loadUser() {
40
+ try { return JSON.parse(localStorage.getItem(USER_KEY) || "null"); }
41
+ catch (e) { return null; }
42
+ }
43
+ // SHA-256 bila tersedia (konteks aman); fallback FNV-1a bila tidak
44
+ // (mis. dibuka via http://IP non-localhost). Fallback ini bukan pengaman.
45
+ function hashPass(pw) {
46
+ if (window.crypto && crypto.subtle) {
47
+ return crypto.subtle.digest("SHA-256", new TextEncoder().encode(pw))
48
+ .then(function (buf) {
49
+ return Array.prototype.map.call(new Uint8Array(buf), function (b) {
50
+ return ("0" + b.toString(16)).slice(-2);
51
+ }).join("");
52
+ });
53
+ }
54
+ var h = 0x811c9dc5;
55
+ for (var i = 0; i < pw.length; i++) {
56
+ h ^= pw.charCodeAt(i);
57
+ h = Math.imul(h, 0x01000193);
58
+ }
59
+ return Promise.resolve("fnv-" + (h >>> 0).toString(16));
60
+ }
61
+ function fail(msg) { errEl.textContent = msg; }
62
+
63
+ try {
64
+ if (sessionStorage.getItem(SESSION_KEY)) { location.replace("index.html"); return; }
65
+ } catch (e) {}
66
+
67
+ var existing = loadUser();
68
+ if (!existing) {
69
+ btn.textContent = "Buat Akun & Masuk";
70
+ document.querySelector(".login-card .sub").textContent =
71
+ "Belum ada akun di perangkat ini — akun pertama yang dibuat menjadi akun perangkat ini.";
72
+ } else {
73
+ resetBtn.classList.remove("hidden");
74
+ resetBtn.addEventListener("click", function () {
75
+ try { localStorage.removeItem(USER_KEY); } catch (e) {}
76
+ location.reload();
77
+ });
78
+ }
79
+
80
+ form.addEventListener("submit", function (e) {
81
+ e.preventDefault();
82
+ var username = userEl.value.trim();
83
+ var password = passEl.value;
84
+ if (!username || !password) { fail("Isi nama pengguna dan kata sandi."); return; }
85
+ errEl.textContent = "";
86
+ var saved = loadUser();
87
+ hashPass(password).then(function (hash) {
88
+ if (!saved) {
89
+ try { localStorage.setItem(USER_KEY, JSON.stringify({ username: username, passHash: hash })); }
90
+ catch (err) { fail("Gagal menyimpan akun di perangkat ini."); return; }
91
+ } else if (saved.username !== username || saved.passHash !== hash) {
92
+ fail("Nama pengguna atau kata sandi salah."); return;
93
+ }
94
+ try { sessionStorage.setItem(SESSION_KEY, username); } catch (err) {}
95
+ location.href = "index.html";
96
+ });
97
+ });
98
+ })();
99
+ </script>
100
+ </body>
101
+ </html>
package/package.json CHANGED
@@ -1,6 +1,26 @@
1
1
  {
2
+ "bin": {
3
+ "rutbhics": "bin/rutbhics.js"
4
+ },
5
+ "description": "Rutbhics — desktop komputer di dalam browser: window manager, taskbar, menu Start, dan aplikasi (Obrolan, Peramban, Files, Catatan, Kalkulator) + halaman login. Murni HTML/CSS/JavaScript vanilla, tanpa build step.",
6
+ "files": [
7
+ "index.html",
8
+ "login.html",
9
+ "css/",
10
+ "js/",
11
+ "README.md",
12
+ "LICENSE",
13
+ "bin/"
14
+ ],
15
+ "homepage": "https://github.com/SeventID/Rutbhics#readme",
16
+ "license": "MIT",
2
17
  "name": "rutbhics",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/SeventID/Rutbhics.git"
21
+ },
22
+ "scripts": {
23
+ "start": "node bin/rutbhics.js"
24
+ },
25
+ "version": "1.0.0"
6
26
  }