diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f33f2a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/node_modules/* +/package-lock.json +/npm-debug.log +.vscode +localdb.js +io.js +init.js +context \ No newline at end of file diff --git a/DOCS.test.js b/DOCS.test.js new file mode 100644 index 0000000..67b6c8b --- /dev/null +++ b/DOCS.test.js @@ -0,0 +1,243 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) +const docs_path = './src/node_modules/DOCS/index.js' + +describe('DOCS isolated handlers', () => { + let DOCS + + beforeEach(() => { + delete global.__DOCS_GLOBAL_STATE__ + delete require.cache[require.resolve(docs_path)] + DOCS = require(docs_path) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + function create_docs () { + return DOCS('test_component.js')('sid_1') + } + + function create_action () { + return { + name: 'Open File', + info: 'Open the selected file.', + icon: 'file', + status: {}, + steps: [] + } + } + + it('accepts only function handlers and exposes only the isolated API', async () => { + const docs = create_docs() + const receiver = {} + const event = {} + + function inspect (event, $) { + event.receiver = this + event.keys = Object.keys($) + } + + expect(() => docs.wrap_isolated('function () {}')).toThrow('must be a function') + expect(Object.keys(docs).sort()).toEqual([ + 'admin', + 'clear_handler_docs', + 'get_docs_mode', + 'get_toc', + 'on_docs_mode_change', + 'register_actions', + 'wrap_isolated' + ]) + + await docs.wrap_isolated(inspect).call(receiver, event) + expect(event.receiver).toBe(receiver) + expect(event.keys).toEqual(['state']) + }) + + it('blocks the event and shows handler info in docs mode', async () => { + const docs = create_docs() + const displays = [] + const event = { preventDefault: vi.fn(), stopPropagation: vi.fn() } + + function on_click (event) { + event.ran = true + return 'clicked' + } + on_click.info = 'Click docs.' + + docs.admin.set_doc_display_handler(display => displays.push(display)) + docs.admin.set_docs_mode(true) + + await expect(docs.wrap_isolated(on_click)(event)).resolves.toBe('clicked') + expect(event.ran).toBe(true) + expect(event.preventDefault).toHaveBeenCalled() + expect(event.stopPropagation).toHaveBeenCalled() + expect(displays).toEqual([{ content: 'Click docs.', sid: 'sid_1' }]) + }) + + it('lists, deduplicates, and clears handler docs', () => { + const docs = create_docs() + + function first () {} + function second () {} + function third () {} + first.info = '# Same\nDocs.' + second.info = '# Same\nDocs.' + third.info = '# Other\nDocs.' + + docs.register_actions([create_action()]) + docs.wrap_isolated(first) + docs.wrap_isolated(second) + docs.wrap_isolated(third) + + expect(docs.get_toc().actions[0].name).toBe('Open File') + expect(docs.get_toc().handlers.map(entry => entry.doc)).toEqual(['# Same\nDocs.', '# Other\nDocs.']) + + docs.clear_handler_docs() + expect(docs.get_toc().handlers).toEqual([]) + }) + + it('keeps hidden actions dispatchable but out of public action lists', async () => { + const docs = create_docs() + const run = vi.fn() + const hidden_action = { + ...create_action(), + name: 'Focus Wizard Hat', + status: { hidden: true }, + run + } + + function on_focus (event, $) { $('Focus Wizard Hat') } + + docs.register_actions([create_action(), hidden_action]) + + expect(docs.admin.get_actions('sid_1').map(action => action.name)).toEqual(['Open File']) + expect(docs.get_toc().actions.map(action => action.name)).toEqual(['Open File']) + await docs.wrap_isolated(on_focus)({}) + expect(run).toHaveBeenCalledOnce() + + docs.register_actions([hidden_action]) + expect(docs.admin.get_actions('sid_1')).toEqual([]) + await docs.wrap_isolated(on_focus)({}) + expect(run).toHaveBeenCalledTimes(2) + }) + + it('runs registered action closures from function handlers', async () => { + const docs = create_docs() + const interaction_state = { count: 0 } + const run = vi.fn(() => 'completed') + const action = { ...create_action(), run } + + function on_click (event, $) { + $.state.count += 1 + if ($.state.count === 2) $('open_file') + return $.state.count + } + on_click.info = 'Count clicks.' + on_click.opts = { state: interaction_state } + + docs.register_actions([action]) + const handler = docs.wrap_isolated(on_click) + + expect(docs.get_toc().handlers[0].doc).toBe('Count clicks.') + expect(await handler({})).toBe(1) + expect(await handler({})).toBe('completed') + expect(run).toHaveBeenCalledOnce() + expect(interaction_state.count).toBe(2) + expect(docs.admin.get_actions('sid_1')[0].run).toBeUndefined() + }) + + it('shares disposable docs state across isolated handlers', async () => { + const docs = create_docs() + const displays = [] + const interaction_state = { count: 0 } + const run = vi.fn() + + function add (event, $) { + $.state.count += event.value + return $.state.count + } + function submit (event, $) { + if ($.state.count === 2) $('Open File') + } + add.info = 'Add input.' + submit.info = 'Submit input.' + add.opts = { state: interaction_state } + submit.opts = { state: interaction_state } + + docs.register_actions([{ ...create_action(), run }]) + docs.admin.set_doc_display_handler(display => displays.push(display)) + docs.admin.set_docs_mode(true) + + const add_handler = docs.wrap_isolated(add) + const submit_handler = docs.wrap_isolated(submit) + expect(await add_handler({ value: 2 })).toBe(2) + await submit_handler({}) + + expect(interaction_state.count).toBe(0) + expect(run).not.toHaveBeenCalled() + expect(displays.map(display => display.content)).toEqual(['Add input.', 'Open the selected file.']) + + docs.admin.set_docs_mode(false) + await add_handler({ value: 1 }) + docs.admin.set_docs_mode(true) + await submit_handler({}) + + expect(interaction_state.count).toBe(1) + expect(displays.at(-1).content).toBe('Submit input.') + }) + + it('rejects invalid requests and propagates action failures', async () => { + const docs = create_docs() + const failure = new Error('action failed') + + function unknown (event, $) { $('Unknown') } + function open_file (event, $) { $('Open File') } + function repeated (event, $) { + $('Open File') + $('Open File') + } + + await expect(docs.wrap_isolated(unknown)({})).rejects.toThrow('Unknown action') + docs.register_actions([create_action()]) + await expect(docs.wrap_isolated(open_file)({})).rejects.toThrow('has no run callback') + await expect(docs.wrap_isolated(repeated)({})).rejects.toThrow('already requested an action') + + docs.register_actions([{ ...create_action(), run: () => { throw failure } }]) + await expect(docs.wrap_isolated(open_file)({})).rejects.toThrow(failure) + }) + + it('does not give handlers access to their closure', async () => { + const docs = create_docs() + const closure_value = 'private' + + function on_click (event) { event.value = closure_value } + + await expect(docs.wrap_isolated(on_click)({})).rejects.toThrow(ReferenceError) + }) + + it('propagates documentation display failures', async () => { + const docs = create_docs() + const failure = new Error('display failed') + + function on_click () {} + on_click.info = 'Click docs.' + + docs.admin.set_doc_display_handler(() => Promise.reject(failure)) + docs.admin.set_docs_mode(true) + + await expect(docs.wrap_isolated(on_click)({})).rejects.toThrow(failure) + }) + + it('validates action info and rejects ambiguous aliases', () => { + const docs = create_docs() + const invalid_action = { ...create_action(), info: '' } + const alias_collision = { ...create_action(), name: 'open_file' } + + expect(() => docs.register_actions([invalid_action])).toThrow("Invalid 'info'") + expect(() => docs.register_actions([create_action(), alias_collision])).toThrow('Duplicate action key "open_file"') + }) +}) diff --git a/README.md b/README.md index 118427f..068b6e2 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,15 @@ # ui-components see https://playproject.io/ui-components/ + + +This is a showcase place for the Theme Widget app components. +This app is a simple app that allows you to change the theme of a website and see the changes in real time. + +To clone and install, run the following commands: + +```bash +git clone https://github.com/ddroid/ui-components +cd ui-components +npm install +npm run start +``` \ No newline at end of file diff --git a/bundle.js b/bundle.js new file mode 100644 index 0000000..30fbf6a --- /dev/null +++ b/bundle.js @@ -0,0 +1,13095 @@ +(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i + + + ` + const searchbar = shadow.querySelector('.searchbar') + const menubar = shadow.querySelector('.menubar') + const container = shadow.querySelector('.graph-container') + + document.body.style.margin = 0 + + let scroll_update_pending = false + container.onscroll = onscroll + + let start_index = 0 + let end_index = 0 + const chunk_size = 50 + const max_rendered_nodes = chunk_size * 3 + let node_height + + const top_sentinel = document.createElement('div') + const bottom_sentinel = document.createElement('div') + + const observer = new IntersectionObserver(handle_sentinel_intersection, { + root: container, + rootMargin: '500px 0px', + threshold: 0 + }) + + // Define handlers for different data types from the drive, called by `onbatch`. + const on = { + style: inject_style, + runtime: on_runtime, + mode: on_mode, + flags: on_flags, + keybinds: on_keybinds, + undo: on_undo + } + // Start watching for state changes. This is the main trigger for all updates. + await sdb.watch(onbatch) + + document.onkeydown = handle_keyboard_navigation + + return el + + /****************************************************************************** + ESSAGE HANDLING + - Handles incoming messages and sends outgoing messages. + - Messages follow standardized net_helper format: { head, refs, type, data, meta } + ******************************************************************************/ + function onmessage (msg) { + const { type, data } = msg + const on_message_types = { + set_mode: handle_set_mode, + set_search_query: handle_set_search_query, + select_nodes: handle_select_nodes, + expand_node: handle_expand_node, + collapse_node: handle_collapse_node, + toggle_node: handle_toggle_node, + get_selected: handle_get_selected, + get_confirmed: handle_get_confirmed, + clear_selection: handle_clear_selection, + set_flag: handle_set_flag, + scroll_to_node: handle_scroll_to_node, + db_response: handle_db_response, + db_initialized: handle_db_initialized + } + + const handler = on_message_types[type] + if (handler) handler(data) + else console.warn(`[graph_explorer-protocol] Unknown message type: ${type}`, msg) + + function handle_db_response () { + db.handle_response(msg) + } + + function handle_set_mode (data) { + const { mode: new_mode } = data + if (new_mode && ['default', 'menubar', 'search'].includes(new_mode)) { + update_drive_state({ type: 'mode/current_mode', message: new_mode }) + send_message({ type: 'mode_changed', refs: { cause: msg.head }, data: { mode: new_mode } }) + } + } + + function handle_set_search_query (data) { + const { query } = data + if (typeof query === 'string') { + search_query = query + drive_updated_by_search = true + update_drive_state({ type: 'mode/search_query', message: query }) + if (mode === 'search') perform_search(query) + send_message({ type: 'search_query_changed', refs: { cause: msg.head }, data: { query } }) + } + } + + function handle_select_nodes (data) { + const { instance_paths } = data + if (Array.isArray(instance_paths)) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: instance_paths }) + send_message({ type: 'selection_changed', refs: { cause: msg.head }, data: { selected: instance_paths } }) + } + } + + function handle_expand_node (data) { + const { instance_path, expand_subs = true, expand_hubs = false } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = expand_subs + instance_states[instance_path].expanded_hubs = expand_hubs + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_expanded', refs: { cause: msg.head }, data: { instance_path, expand_subs, expand_hubs } }) + } + } + + function handle_collapse_node (data) { + const { instance_path } = data + if (instance_path && instance_states[instance_path]) { + instance_states[instance_path].expanded_subs = false + instance_states[instance_path].expanded_hubs = false + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'node_collapsed', refs: { cause: msg.head }, data: { instance_path } }) + } + } + + async function handle_toggle_node (data) { + const { instance_path, toggle_type = 'subs' } = data + if (instance_path && instance_states[instance_path]) { + if (toggle_type === 'subs') { + await toggle_subs(instance_path) + } else if (toggle_type === 'hubs') { + await toggle_hubs(instance_path) + } + send_message({ type: 'node_toggled', refs: { cause: msg.head }, data: { instance_path, toggle_type } }) + } + } + + function handle_get_selected (data) { + send_message({ type: 'selected_nodes', refs: { cause: msg.head }, data: { selected: selected_instance_paths } }) + } + + function handle_get_confirmed (data) { + send_message({ type: 'confirmed_nodes', refs: { cause: msg.head }, data: { confirmed: confirmed_instance_paths } }) + } + + function handle_clear_selection (data) { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + send_message({ type: 'selection_cleared', refs: { cause: msg.head }, data: {} }) + } + + function handle_set_flag (data) { + const { flag_type, value } = data + if (flag_type === 'hubs' && ['default', 'true', 'false'].includes(value)) { + update_drive_state({ type: 'flags/hubs', message: value }) + } else if (flag_type === 'selection') { + update_drive_state({ type: 'flags/selection', message: value }) + } else if (flag_type === 'recursive_collapse') { + update_drive_state({ type: 'flags/recursive_collapse', message: value }) + } + send_message({ type: 'flag_changed', refs: { cause: msg.head }, data: { flag_type, value } }) + } + + function handle_scroll_to_node (data) { + const { instance_path } = data + const node_index = view.findIndex(n => n.instance_path === instance_path) + if (node_index !== -1) { + const scroll_position = node_index * node_height + container.scrollTop = scroll_position + send_message({ type: 'scrolled_to_node', refs: { cause: msg.head }, data: { instance_path, scroll_position } }) + } + } + } + async function handle_db_initialized (data) { + // Page.js, trigger initial render + // After receiving entries, ensure the root node state is initialized and trigger the first render. + const root_path = '/' + if (await db.has(root_path)) { + const root_instance_path = '|/' + if (!instance_states[root_instance_path]) { + instance_states[root_instance_path] = { + expanded_subs: true, + expanded_hubs: false + } + } + // don't rebuild view if we're in search mode with active query + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_entries: skipping build_and_render_view in Search Mode with query:', search_query) + perform_search(search_query) + } else { + // tracking will be initialized later if drive data is empty + build_and_render_view() + } + } else { + console.warn('Root path "/" not found in entries. Clearing view.') + view = [] + if (container) container.replaceChildren() + } + } + function send_message ({ type, refs = {}, data = {} }) { + if (!_.storage) throw new Error('graph_explorer net_helper channel "storage" is not connected') + return _.storage(type, refs, data) + } + + function create_db () { + // Pending requests map: key is net_helper message head, value is {resolve, reject} + const pending_requests = new Map() + const early_responses = new Map() + + return { + // All operations are async via protocol messages + get: (path) => send_db_request('db_get', { path }), + has: (path) => send_db_request('db_has', { path }), + is_empty: () => send_db_request('db_is_empty', {}), + root: () => send_db_request('db_root', {}), + keys: () => send_db_request('db_keys', {}), + raw: () => send_db_request('db_raw', {}), + // Handle responses from page.js + handle_response: (msg) => { + if (!msg.refs || !msg.refs.cause) { + console.warn('[graph_explorer] Response missing refs.cause:', msg) + return + } + const request_head_key = JSON.stringify(msg.refs.cause) + const pending = pending_requests.get(request_head_key) + if (pending) { + pending.resolve(msg.data.result) + pending_requests.delete(request_head_key) + } else { + early_responses.set(request_head_key, msg) + } + } + } + + function send_db_request (operation, params) { + return new Promise((resolve, reject) => { + const head = send_message({ type: operation, refs: {}, data: params }) + const head_key = JSON.stringify(head) + pending_requests.set(head_key, { resolve, reject }) + const early_response = early_responses.get(head_key) + if (early_response) { + early_responses.delete(head_key) + db.handle_response(early_response) + } + }) + } + } + + /****************************************************************************** + STATE AND DATA HANDLING + - These functions process incoming data from the STATE module's `sdb.watch`. + - `onbatch` is the primary entry point. + ******************************************************************************/ + async function onbatch (batch) { + console.log('[SEARCH DEBUG] onbatch caled:', { + mode, + search_query, + last_clicked_node, + feedback_flags: { + scroll: drive_updated_by_scroll, + toggle: drive_updated_by_toggle, + search: drive_updated_by_search, + match: drive_updated_by_match, + tracking: drive_updated_by_tracking + } + }) + + // Prevent feedback loops from scroll or toggle actions. + if (check_and_reset_feedback_flags()) { + console.log('[SEARCH DEBUG] onbatch prevented by feedback flags') + return + } + + for (const { type, paths } of batch) { + if (!paths || !paths.length) continue + const data = await Promise.all( + paths.map(path => batch_get(path)) + ) + // Call the appropriate handler based on `type`. + const func = on[type] + func ? await func({ data, paths }) : fail(data, type) + } + + function batch_get (path) { + return drive + .get(path) + .then(file => (file ? file.raw : null)) + .catch(e => { + console.error(`Error getting file from drive: ${path}`, e) + return null + }) + } + } + + function fail (data, type) { + throw new Error(`Invalid message type: ${type}`, { cause: { data, type } }) + } + + async function on_runtime ({ data, paths }) { + const on_runtime_paths = { + 'node_height.json': handle_node_height, + 'vertical_scroll_value.json': handle_vertical_scroll, + 'horizontal_scroll_value.json': handle_horizontal_scroll, + 'selected_instance_paths.json': handle_selected_paths, + 'confirmed_selected.json': handle_confirmed_paths, + 'instance_states.json': handle_instance_states, + 'search_entry_states.json': handle_search_entry_states, + 'last_clicked_node.json': handle_last_clicked_node, + 'view_order_tracking.json': handle_view_order_tracking + } + let needs_render = false + const render_nodes_needed = new Set() + + paths.forEach((path, i) => runtime_handler(path, data[i])) + + if (needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_runtime: Skipping build_and_render_view in search mode with query:', search_query) + await perform_search(search_query) + } else { + await build_and_render_view() + } + } else if (render_nodes_needed.size > 0) { + render_nodes_needed.forEach(re_render_node) + } + + function runtime_handler (path, data) { + if (data === null) return + const value = parse_json_data(data, path) + if (value === null) return + + // Extract filename from path and use handler if available + const filename = path.split('/').pop() + const handler = on_runtime_paths[filename] + if (handler) { + const result = handler({ value, render_nodes_needed }) + if (result?.needs_render) needs_render = true + } + } + + function handle_node_height ({ value }) { + node_height = value + } + + function handle_vertical_scroll ({ value }) { + if (typeof value === 'number') vertical_scroll_value = value + } + + function handle_horizontal_scroll ({ value }) { + if (typeof value === 'number') horizontal_scroll_value = value + } + + function handle_selected_paths ({ value, render_nodes_needed }) { + selected_instance_paths = process_path_array_update({ + current_paths: selected_instance_paths, + value, + render_set: render_nodes_needed, + name: 'selected_instance_paths' + }) + } + + function handle_confirmed_paths ({ value, render_nodes_needed }) { + confirmed_instance_paths = process_path_array_update({ + current_paths: confirmed_instance_paths, + value, + render_set: render_nodes_needed, + name: 'confirmed_selected' + }) + } + + function handle_instance_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + instance_states = value + return { needs_render: true } + } else { + console.warn('instance_states is not a valid object, ignoring.', value) + } + } + + function handle_search_entry_states ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + search_entry_states = value + if (mode === 'search') return { needs_render: true } + } else { + console.warn('search_entry_states is not a valid object, ignoring.', value) + } + } + + function handle_last_clicked_node ({ value, render_nodes_needed }) { + const old_last_clicked = last_clicked_node + last_clicked_node = typeof value === 'string' ? value : null + if (old_last_clicked) render_nodes_needed.add(old_last_clicked) + if (last_clicked_node) render_nodes_needed.add(last_clicked_node) + } + + function handle_view_order_tracking ({ value }) { + if (typeof value === 'object' && value && !Array.isArray(value)) { + is_loading_from_drive = true + view_order_tracking = value + is_loading_from_drive = false + if (Object.keys(view_order_tracking).length === 0) { + initialize_tracking_from_current_state() + } + return { needs_render: true } + } else { + console.warn('view_order_tracking is not a valid object, ignoring.', value) + } + } + } + + async function on_mode ({ data, paths }) { + const on_mode_paths = { + 'current_mode.json': handle_current_mode, + 'previous_mode.json': handle_previous_mode, + 'search_query.json': handle_search_query, + 'multi_select_enabled.json': handle_multi_select_enabled, + 'select_between_enabled.json': handle_select_between_enabled + } + let new_current_mode, new_previous_mode, new_search_query, new_multi_select_enabled, new_select_between_enabled + + paths.forEach((path, i) => mode_handler(path, data[i])) + + if (typeof new_search_query === 'string') search_query = new_search_query + if (new_previous_mode) previous_mode = new_previous_mode + if (typeof new_multi_select_enabled === 'boolean') { + multi_select_enabled = new_multi_select_enabled + render_menubar() // Re-render menubar to update button text + } + if (typeof new_select_between_enabled === 'boolean') { + select_between_enabled = new_select_between_enabled + if (!select_between_enabled) select_between_first_node = null + render_menubar() + } + + if ( + new_current_mode && + !['default', 'menubar', 'search'].includes(new_current_mode) + ) { + console.warn(`Invalid mode "${new_current_mode}" provided. Ignoring update.`) + return + } + + if (new_current_mode === 'search' && !search_query) { + search_state_instances = instance_states + } + if (!new_current_mode || mode === new_current_mode) return + + if (mode && new_current_mode === 'search') update_drive_state({ type: 'mode/previous_mode', message: mode }) + mode = new_current_mode + render_menubar() + render_searchbar() + await handle_mode_change() + if (mode === 'search' && search_query) await perform_search(search_query) + + function mode_handler (path, data) { + const value = parse_json_data(data, path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_mode_paths[filename] + if (handler) { + const result = handler({ value }) + if (result?.current_mode !== undefined) new_current_mode = result.current_mode + if (result?.previous_mode !== undefined) new_previous_mode = result.previous_mode + if (result?.search_query !== undefined) new_search_query = result.search_query + if (result?.multi_select_enabled !== undefined) new_multi_select_enabled = result.multi_select_enabled + if (result?.select_between_enabled !== undefined) new_select_between_enabled = result.select_between_enabled + } + } + function handle_current_mode ({ value }) { + return { current_mode: value } + } + + function handle_previous_mode ({ value }) { + return { previous_mode: value } + } + + function handle_search_query ({ value }) { + return { search_query: value } + } + + function handle_multi_select_enabled ({ value }) { + return { multi_select_enabled: value } + } + + function handle_select_between_enabled ({ value }) { + return { select_between_enabled: value } + } + } + + function on_flags ({ data, paths }) { + const on_flags_paths = { + 'hubs.json': handle_hubs_flag, + 'selection.json': handle_selection_flag, + 'recursive_collapse.json': handle_recursive_collapse_flag + } + + paths.forEach((path, i) => flags_handler(path, data[i])) + + function flags_handler (path, data) { + const value = parse_json_data(data, path) + if (value === null) return + + const filename = path.split('/').pop() + const handler = on_flags_paths[filename] + if (handler) { + const result = handler(value) + if (result && result.needs_render) { + if (mode === 'search' && search_query) { + console.log('[SEARCH DEBUG] on_flags: Skipping build_and_render_view in search mode with query:', search_query) + perform_search(search_query) + } else { + build_and_render_view() + } + } + } + } + + function handle_hubs_flag (value) { + if (typeof value === 'string' && ['default', 'true', 'false'].includes(value)) { + hubs_flag = value + return { needs_render: true } + } else { + console.warn('hubs flag must be one of: "default", "true", "false", ignoring.', value) + } + } + + function handle_selection_flag (value) { + selection_flag = value + return { needs_render: true } + } + + function handle_recursive_collapse_flag (value) { + recursive_collapse_flag = value + return { needs_render: false } + } + } + + function inject_style ({ data }) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data[0]) + shadow.adoptedStyleSheets = [sheet] + } + + function on_keybinds ({ data }) { + if (!data || data[0] == null) { + console.error('Keybinds data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed keybinds data is not a valid object.') + return + } + keybinds = parsed_data + } + + function on_undo ({ data }) { + if (!data || data[0] == null) { + console.error('Undo stack data is missing or empty.') + return + } + const parsed_data = parse_json_data(data[0]) + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed undo stack data is not a valid Object.') + return + } + undo_stack = parsed_data + } + + // Helper to persist component state to the drive. + async function update_drive_state ({ type, message }) { + // Save current state to undo stack before updating (except for some) + const should_track = ( + !drive_updated_by_undo && + !type.includes('scroll') && + !type.includes('last_clicked') && + !type.includes('view_order_tracking') && + !type.includes('select_between') && + type !== 'undo/stack' + ) + if (should_track) { + await save_to_undo_stack(type) + } + + try { + await drive.put(`${type}.json`, JSON.stringify(message)) + } catch (e) { + const [dataset, name] = type.split('/') + console.error(`Failed to update ${dataset} state for ${name}:`, e) + } + if (should_track) { + render_menubar() + } + } + + async function save_to_undo_stack (type) { + try { + const current_file = await drive.get(`${type}.json`) + if (current_file && current_file.raw) { + const snapshot = { + type, + value: current_file.raw, + timestamp: Date.now() + } + + // Add to stack (limit to 50 items to prevent memory issues) + undo_stack.push(snapshot) + if (undo_stack.length > 50) { + undo_stack.shift() + } + drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + } + } catch (e) { + console.error('Failed to save to undo stack:', e) + } + } + + function get_or_create_state (states, instance_path) { + if (!states[instance_path]) { + states[instance_path] = { expanded_subs: false, expanded_hubs: false } + } + if (states[instance_path].expanded_subs === null) { + states[instance_path].expanded_subs = true + } + + return states[instance_path] + } + + async function calculate_children_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) { + const children_pipe_trail = [...parent_pipe_trail] + const parent_entry = await db.get(parent_base_path) + const is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' + + if (depth > 0) { + if (is_hub) { + if (is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_hub_on_top && !is_last_sub) { + children_pipe_trail.pop() + children_pipe_trail.push(true) + } + if (is_first_hub) { + children_pipe_trail.pop() + children_pipe_trail.push(false) + } + } + children_pipe_trail.push(is_hub || !is_last_sub) + } + return { children_pipe_trail, is_hub_on_top } + } + + // Extracted pipe logic for reuse in both default and search modes + async function calculate_pipe_trail ({ + depth, + is_hub, + is_last_sub, + is_first_hub = false, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) { + let last_pipe = null + const parent_entry = await db.get(parent_base_path) + const calculated_is_hub_on_top = base_path === parent_entry?.hubs?.[0] || base_path === '/' + const final_is_hub_on_top = is_hub_on_top !== undefined ? is_hub_on_top : calculated_is_hub_on_top + + if (depth > 0) { + if (is_hub) { + last_pipe = [...parent_pipe_trail] + if (is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + if (is_first_hub) { + last_pipe.pop() + last_pipe.push(false) + } + } + if (final_is_hub_on_top && !is_last_sub) { + last_pipe.pop() + last_pipe.push(true) + } + } + } + + const pipe_trail = (is_hub && is_last_sub) || (is_hub && final_is_hub_on_top) ? last_pipe : parent_pipe_trail + const product = { pipe_trail, is_hub_on_top: final_is_hub_on_top } + return product + } + + /****************************************************************************** + VIEW AND RENDERING LOGIC AND SCALING + - These functions build the `view` array and render the DOM. + - `build_and_render_view` is the main orchestrator. + - `build_view_recursive` creates the flat `view` array from the hierarchical data. + - `calculate_mobile_scale` calculates the scale factor for mobile devices. + ******************************************************************************/ + async function build_and_render_view (focal_instance_path, hub_toggle = false) { + console.log('[SEARCH DEBUG] build_and_render_view called:', { + focal_instance_path, + hub_toggle, + current_mode: mode, + search_query, + last_clicked_node, + stack_trace: new Error().stack.split('\n').slice(1, 4).map(line => line.trim()) + }) + + // This fuction should'nt be called in search mode for search + if (mode === 'search' && search_query && !hub_toggle) { + console.error('[SEARCH DEBUG] build_and_render_view called inappropriately in search mode!', { + mode, + search_query, + focal_instance_path, + stack_trace: new Error().stack.split('\n').slice(1, 6).map(line => line.trim()) + }) + } + + const is_empty = await db.is_empty() + if (!db || is_empty) { + console.warn('No entries available to render.') + return + } + + const old_view = [...view] + const old_scroll_top = vertical_scroll_value + const old_scroll_left = horizontal_scroll_value + let existing_spacer_height = 0 + if (spacer_element && spacer_element.parentNode) existing_spacer_height = parseFloat(spacer_element.style.height) || 0 + + // Recursively build the new `view` array from the graph data. + view = await build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub: true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + db + }) + + // Recalculate duplicates after view is built + collect_all_duplicate_entries() + + const new_scroll_top = calculate_new_scroll_top({ + old_scroll_top, + old_view, + focal_path: focal_instance_path + }) + const render_anchor_index = Math.max(0, Math.floor(new_scroll_top / node_height)) + start_index = Math.max(0, render_anchor_index - chunk_size) + end_index = Math.min(view.length, render_anchor_index + chunk_size) + + const fragment = document.createDocumentFragment() + for (let i = start_index; i < end_index; i++) { + if (view[i]) fragment.appendChild(create_node(view[i])) + } + + container.replaceChildren(top_sentinel, fragment, bottom_sentinel) + top_sentinel.style.height = `${start_index * node_height}px` + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + + observer.observe(top_sentinel) + observer.observe(bottom_sentinel) + + // Handle the spacer element used for keep entries static wrt cursor by scrolling when hubs are toggled. + handle_spacer_element({ + hub_toggle, + existing_height: existing_spacer_height, + new_scroll_top, + sync_fn: set_scroll_and_sync + }) + + function set_scroll_and_sync () { + drive_updated_by_scroll = true + container.scrollTop = new_scroll_top + container.scrollLeft = old_scroll_left + vertical_scroll_value = container.scrollTop + } + } + + // Traverses the hierarchical entries data and builds a flat `view` array for rendering. + async function build_view_recursive ({ + base_path, + parent_instance_path, + parent_base_path = null, + depth, + is_last_sub, + is_hub, + is_first_hub = false, + parent_pipe_trail, + instance_states, + db + }) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return [] + + const state = get_or_create_state(instance_states, instance_path) + + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + + const current_view = [] + // If hubs are expanded, recursively add them to the view first (they appear above the node). + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_view_recursive({ + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub ? is_hub_on_top : false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...hub_view) + } + } + + // Calculate pipe_trail for this node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + + current_view.push({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + entry, // Include entry data in view to avoid async lookups during rendering + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position + }) + + // If subs are expanded, recursively add them to the view (they appear below the node). + if (state.expanded_subs && Array.isArray(entry.subs)) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_view_recursive({ + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db + }) + current_view.push(...sub_view) + } + } + return current_view + } + + /****************************************************************************** + 4. NODE CREATION AND EVENT HANDLING + - `create_node` generates the DOM element for a single node. + - It sets up event handlers for user interactions like selecting or toggling. + ******************************************************************************/ + + function create_node ({ + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + is_search_match, + is_direct_match, + is_in_original_view, + query, + entry, // Entry data is now passed from view + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top // Pre-calculated hub position + }) { + if (!entry) { + const err_el = document.createElement('div') + err_el.className = 'node error' + err_el.textContent = `Error: Missing entry for ${base_path}` + return err_el + } + + let states + if (mode === 'search') { + if (manipulated_inside_search[instance_path]) { + search_entry_states[instance_path] = manipulated_inside_search[instance_path] + states = search_entry_states + } else { + states = search_state_instances + } + } else { + states = instance_states + } + const state = get_or_create_state(states, instance_path) + + const el = document.createElement('div') + el.className = `node type-${entry.type || 'unknown'}` + el.dataset.instance_path = instance_path + if (is_search_match) { + el.classList.add('search-result') + if (is_direct_match) el.classList.add('direct-match') + if (!is_in_original_view) el.classList.add('new-entry') + } + + if (selected_instance_paths.includes(instance_path)) el.classList.add('selected') + if (confirmed_instance_paths.includes(instance_path)) el.classList.add('confirmed') + if (last_clicked_node === instance_path) { + mode === 'search' ? el.classList.add('search-last-clicked') : el.classList.add('last-clicked') + } + + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry.hubs) && entry.hubs.length > 0 + const has_subs = Array.isArray(entry.subs) && entry.subs.length > 0 + + if (depth) { + el.classList.add('left-indent') + } + + if (base_path === '/' && instance_path === '|/') return create_root_node({ state, has_subs, instance_path }) + const prefix_class_name = get_prefix({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) + // Use pre-calculated pipe_trail + const pipe_html = pipe_trail.map(p => ``).join('') + const prefix_class = has_subs ? 'prefix clickable' : 'prefix' + const icon_class = has_hubs && base_path !== '/' ? 'icon clickable' : 'icon' + const entry_name = entry.name || base_path + const name_html = (is_direct_match && query) + ? get_highlighted_name(entry_name, query) + : entry_name + + // Check if this entry appears elsewhere in the view (any duplicate) + let has_duplicate_entries = false + let is_first_occurrence = false + if (hubs_flag !== 'true') { + has_duplicate_entries = has_duplicates(base_path) + + // coloring class for duplicates + if (has_duplicate_entries) { + is_first_occurrence = is_first_duplicate(base_path, instance_path) + if (is_first_occurrence) { + el.classList.add('first-matching-entry') + } else { + el.classList.add('matching-entry') + } + } + } + + el.innerHTML = ` + ${pipe_html} + + + ${name_html} + ` + + // For matching entries, disable normal event listener and add handler to whole entry to create button for jump to next duplicate + if (has_duplicate_entries && !is_first_occurrence && hubs_flag !== 'true') { + el.onclick = jump_out_to_next_duplicate + } else { + const icon_el = el.querySelector('.icon') + if (icon_el && has_hubs && base_path !== '/') { + icon_el.onclick = (mode === 'search' && search_query) + ? () => toggle_search_hubs(instance_path) + : () => toggle_hubs(instance_path) + } + + // Add click event to the whole first part (indent + prefix) for expanding/collapsing subs + if (has_subs) { + const indent_el = el.querySelector('.indent') + const prefix_el = el.querySelector('.prefix') + + const toggle_subs_handler = (mode === 'search' && search_query) + ? () => toggle_search_subs(instance_path) + : () => toggle_subs(instance_path) + + if (indent_el) indent_el.onclick = toggle_subs_handler + if (prefix_el) prefix_el.onclick = toggle_subs_handler + } + + // Special handling for first duplicate entry - it should have normal select behavior but also show jump button + const name_el = el.querySelector('.name') + if (selection_flag !== false) { + if (has_duplicate_entries && is_first_occurrence && hubs_flag !== 'true') { + name_el.onclick = ev => jump_and_select_matching_entry(ev, instance_path) + } else { + name_el.onclick = ev => mode === 'search' ? handle_search_name_click(ev, instance_path) : select_node(ev, instance_path) + } + } else { + name_el.onclick = () => handle_last_clicked_node(instance_path) + } + + function handle_last_clicked_node (instance_path) { + last_clicked_node = instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) + } + } + + if (selected_instance_paths.includes(instance_path) || confirmed_instance_paths.includes(instance_path)) el.appendChild(create_confirm_checkbox(instance_path)) + + return el + function jump_and_select_matching_entry (ev, instance_path) { + if (mode === 'search') { + handle_search_name_click(ev, instance_path) + } else { + select_node(ev, instance_path) + } + // Also add jump button functionality for first occurrence + setTimeout(() => add_jump_button_to_matching_entry(el, base_path, instance_path), 10) + } + function jump_out_to_next_duplicate () { + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + update_last_clicked_styling(instance_path) + add_jump_button_to_matching_entry(el, base_path, instance_path) + } + } + + // `re_render_node` updates a single node in the DOM, used when only its selection state changes. + function re_render_node (instance_path) { + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data) { + const old_node_el = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (old_node_el) old_node_el.replaceWith(create_node(node_data)) + } + } + + // `get_prefix` determines which box-drawing character to use for the node's prefix. It gives the name of a specific CSS class. + function get_prefix ({ is_last_sub, has_subs, state, is_hub, is_hub_on_top }) { + if (!state) { + console.error('get_prefix called with invalid state.') + return 'middle-line' + } + + // Define handlers for different prefix types based on node position + const on_prefix_types = { + hub_on_top: get_hub_on_top_prefix, + hub_not_on_top: get_hub_not_on_top_prefix, + last_sub: get_last_sub_prefix, + middle_sub: get_middle_sub_prefix + } + // Determine the prefix type based on node position + let prefix_type + if (is_hub && is_hub_on_top) prefix_type = 'hub_on_top' + else if (is_hub && !is_hub_on_top) prefix_type = 'hub_not_on_top' + else if (is_last_sub) prefix_type = 'last_sub' + else prefix_type = 'middle_sub' + + const handler = on_prefix_types[prefix_type] + + return handler ? handler({ state, has_subs }) : 'middle-line' + + function get_hub_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'top-cross' + if (expanded_subs) return 'top-tee-down' + if (expanded_hubs) return 'top-tee-up' + return 'top-line' + } + + function get_hub_not_on_top_prefix ({ state }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return 'middle-tee-up' + return 'middle-line' + } + + function get_last_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'bottom-cross' + if (expanded_subs) return 'bottom-tee-down' + if (expanded_hubs) return has_subs ? 'bottom-tee-up' : 'bottom-light-tee-up' + return has_subs ? 'bottom-line' : 'bottom-light-line' + } + + function get_middle_sub_prefix ({ state, has_subs }) { + const { expanded_subs, expanded_hubs } = state + if (expanded_subs && expanded_hubs) return 'middle-cross' + if (expanded_subs) return 'middle-tee-down' + if (expanded_hubs) return has_subs ? 'middle-tee-up' : 'middle-light-tee-up' + return has_subs ? 'middle-line' : 'middle-light-line' + } + } + + /****************************************************************************** + MENUBAR AND SEARCH + ******************************************************************************/ + function render_menubar () { + const search_button = document.createElement('button') + search_button.textContent = 'Search' + search_button.onclick = toggle_search_mode + + const undo_button = document.createElement('button') + undo_button.textContent = `Undo (${undo_stack.length})` + undo_button.onclick = () => undo(1) + undo_button.disabled = undo_stack.length === 0 + + const multi_select_button = document.createElement('button') + multi_select_button.textContent = `Multi Select: ${multi_select_enabled}` + multi_select_button.onclick = toggle_multi_select + + const select_between_button = document.createElement('button') + select_between_button.textContent = `Select Between: ${select_between_enabled}` + select_between_button.onclick = toggle_select_between + + const hubs_button = document.createElement('button') + hubs_button.textContent = `Hubs: ${hubs_flag}` + hubs_button.onclick = toggle_hubs_flag + + const selection_button = document.createElement('button') + selection_button.textContent = `Selection: ${selection_flag}` + selection_button.onclick = toggle_selection_flag + + const recursive_collapse_button = document.createElement('button') + recursive_collapse_button.textContent = `Recursive Collapse: ${recursive_collapse_flag}` + recursive_collapse_button.onclick = toggle_recursive_collapse_flag + + menubar.replaceChildren(search_button, undo_button, multi_select_button, select_between_button, hubs_button, selection_button, recursive_collapse_button) + } + + function render_searchbar () { + if (mode !== 'search') { + searchbar.style.display = 'none' + searchbar.replaceChildren() + return + } + + const search_opts = { + type: 'text', + placeholder: 'Search entries...', + className: 'search-input', + value: search_query, + oninput: on_search_input + } + searchbar.style.display = 'flex' + const search_input = Object.assign(document.createElement('input'), search_opts) + + searchbar.replaceChildren(search_input) + requestAnimationFrame(() => search_input.focus()) + } + + async function handle_mode_change () { + menubar.style.display = mode === 'default' ? 'none' : 'flex' + render_searchbar() + await build_and_render_view() + } + + async function toggle_search_mode () { + const target_mode = mode === 'search' ? previous_mode : 'search' + console.log('[SEARCH DEBUG] Switching mode from', mode, 'to', target_mode) + send_message({ type: 'mode_toggling', data: { from: mode, to: target_mode } }) + if (mode === 'search') { + // When switching from search to default mode, expand selected entries + if (selected_instance_paths.length > 0) { + console.log('[SEARCH DEBUG] Expanding selected entries in default mode:', selected_instance_paths) + await expand_selected_entries_in_default(selected_instance_paths) + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + } + // Reset select-between mode when leaving search mode + if (select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + console.log('[SEARCH DEBUG] Reset select-between mode when leaving search') + } + search_query = '' + update_drive_state({ type: 'mode/search_query', message: '' }) + } + ignore_drive_updated_by_scroll = true + update_drive_state({ type: 'mode/current_mode', message: target_mode }) + search_state_instances = instance_states + send_message({ type: 'mode_changed', data: { mode: target_mode } }) + } + + function toggle_multi_select () { + multi_select_enabled = !multi_select_enabled + // Disable select between when enabling multi select + if (multi_select_enabled && select_between_enabled) { + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + } + update_drive_state({ type: 'mode/multi_select_enabled', message: multi_select_enabled }) + render_menubar() // Re-render to update button text + } + + function toggle_select_between () { + select_between_enabled = !select_between_enabled + select_between_first_node = null // Reset first node selection + // Disable multi select when enabling select between + if (select_between_enabled && multi_select_enabled) { + multi_select_enabled = false + update_drive_state({ type: 'mode/multi_select_enabled', message: false }) + } + update_drive_state({ type: 'mode/select_between_enabled', message: select_between_enabled }) + render_menubar() // Re-render to update button text + } + + function toggle_hubs_flag () { + const values = ['default', 'true', 'false'] + const current_index = values.indexOf(hubs_flag) + const next_index = (current_index + 1) % values.length + hubs_flag = values[next_index] + update_drive_state({ type: 'flags/hubs', message: hubs_flag }) + render_menubar() + } + + function toggle_selection_flag () { + selection_flag = !selection_flag + update_drive_state({ type: 'flags/selection', message: selection_flag }) + render_menubar() + } + + function toggle_recursive_collapse_flag () { + recursive_collapse_flag = !recursive_collapse_flag + update_drive_state({ type: 'flags/recursive_collapse', message: recursive_collapse_flag }) + render_menubar() + } + + function on_search_input (event) { + search_query = event.target.value.trim() + drive_updated_by_search = true + update_drive_state({ type: 'mode/search_query', message: search_query }) + if (search_query === '') search_state_instances = instance_states + perform_search(search_query) + } + + async function perform_search (query) { + console.log('[SEARCH DEBUG] perform_search called:', { + query, + current_mode: mode, + search_query_var: search_query, + has_search_entry_states: Object.keys(search_entry_states).length > 0, + last_clicked_node + }) + if (!query) { + console.log('[SEARCH DEBUG] No query provided, building default view') + return build_and_render_view() + } + + const original_view = await build_view_recursive({ + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub: true, + is_hub: false, + parent_pipe_trail: [], + instance_states, + db + }) + const original_view_paths = original_view.map(n => n.instance_path) + search_state_instances = {} + const search_tracking = {} + const search_view = await build_search_view_recursive({ + query, + base_path: '/', + parent_instance_path: '', + depth: 0, + is_last_sub: true, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: [], + instance_states: search_state_instances, + db, + original_view_paths, + is_expanded_child: false, + search_tracking + }) + console.log('[SEARCH DEBUG] Search view built:', search_view.length) + render_search_results(search_view, query) + } + + async function build_search_view_recursive ({ + query, + base_path, + parent_instance_path, + parent_base_path = null, + depth, + is_last_sub, + is_hub, + is_first_hub = false, + parent_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child = false, + search_tracking = {} + }) { + const entry = await db.get(base_path) + if (!entry) return [] + + const instance_path = `${parent_instance_path}|${base_path}` + const is_direct_match = entry.name && entry.name.toLowerCase().includes(query.toLowerCase()) + + // track instance for duplicate detection + if (!search_tracking[base_path]) search_tracking[base_path] = [] + const is_first_occurrence_in_search = !search_tracking[base_path].length + search_tracking[base_path].push(instance_path) + + // Use extracted pipe logic for consistent rendering + const { children_pipe_trail, is_hub_on_top } = await calculate_children_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + + // Process hubs if they should be expanded + const search_state = search_entry_states[instance_path] + const should_expand_hubs = search_state ? search_state.expanded_hubs : false + const should_expand_subs = search_state ? search_state.expanded_subs : false + + // Process hubs: if manually expanded, show ALL hubs regardless of search match + const hub_results = [] + if (should_expand_hubs && entry.hubs) { + for (let i = 0; i < entry.hubs.length; i++) { + const hub_path = entry.hubs[i] + const hub_view = await build_search_view_recursive({ + query, + base_path: hub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.hubs.length - 1, + is_hub: true, + is_first_hub: is_hub_on_top, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: true, + search_tracking + }) + hub_results.push(...hub_view) + } + } + + // Handle subs: if manually expanded, show ALL children; otherwise, search through them + const sub_results = [] + if (should_expand_subs) { + // Show ALL subs when manually expanded + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: true, + search_tracking + }) + sub_results.push(...sub_view) + } + } + } else if (!is_expanded_child && is_first_occurrence_in_search) { + // Only search through subs for the first occurrence of this base_path + if (entry.subs) { + for (let i = 0; i < entry.subs.length; i++) { + const sub_path = entry.subs[i] + const sub_view = await build_search_view_recursive({ + query, + base_path: sub_path, + parent_instance_path: instance_path, + parent_base_path: base_path, + depth: depth + 1, + is_last_sub: i === entry.subs.length - 1, + is_hub: false, + is_first_hub: false, + parent_pipe_trail: children_pipe_trail, + instance_states, + db, + original_view_paths, + is_expanded_child: false, + search_tracking + }) + sub_results.push(...sub_view) + } + } + } + + const has_matching_descendant = sub_results.length > 0 + + // If this is an expanded child, always include it regardless of search match + // only include if it's the first occurrence OR if a dirct match + if (!is_expanded_child && !is_direct_match && !has_matching_descendant) return [] + if (!is_expanded_child && !is_first_occurrence_in_search && !is_direct_match) return [] + + const final_expand_subs = search_state ? search_state.expanded_subs : (has_matching_descendant && is_first_occurrence_in_search) + const final_expand_hubs = search_state ? search_state.expanded_hubs : false + + instance_states[instance_path] = { expanded_subs: final_expand_subs, expanded_hubs: final_expand_hubs } + const is_in_original_view = original_view_paths.includes(instance_path) + + // Calculate pipe_trail for this search node + const { pipe_trail, is_hub_on_top: calculated_is_hub_on_top } = await calculate_pipe_trail({ + depth, + is_hub, + is_last_sub, + is_first_hub, + is_hub_on_top, + parent_pipe_trail, + parent_base_path, + base_path, + db + }) + + const current_node_view = { + base_path, + instance_path, + depth, + is_last_sub, + is_hub, + is_first_hub, + parent_pipe_trail, + parent_base_path, + is_search_match: true, + is_direct_match, + is_in_original_view, + entry, // Include entry data + pipe_trail, // Pre-calculated pipe trail + is_hub_on_top: calculated_is_hub_on_top // Pre-calculated hub position + } + + return [...hub_results, current_node_view, ...sub_results] + } + + function render_search_results (search_view, query) { + view = search_view + if (search_view.length === 0) { + const no_results_el = document.createElement('div') + no_results_el.className = 'no-results' + no_results_el.textContent = `No results for "${query}"` + return container.replaceChildren(no_results_el) + } + + // temporary tracking map for search results to detect duplicates + const search_tracking = {} + search_view.forEach(node => set_search_tracking(node)) + + const original_tracking = view_order_tracking + view_order_tracking = search_tracking + collect_all_duplicate_entries() + + const fragment = document.createDocumentFragment() + search_view.forEach(node_data => fragment.appendChild(create_node({ ...node_data, query }))) + container.replaceChildren(fragment) + + view_order_tracking = original_tracking + + function set_search_tracking (node) { + const { base_path, instance_path } = node + if (!search_tracking[base_path]) search_tracking[base_path] = [] + search_tracking[base_path].push(instance_path) + } + } + + /****************************************************************************** + VIEW MANIPULATION & USER ACTIONS + - These functions handle user interactions like selecting, confirming, + toggling, and resetting the graph. + ******************************************************************************/ + function select_node (ev, instance_path) { + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + send_message({ type: 'node_clicked', data: { instance_path } }) + + // Handle shift+click to enable select between mode temporarily + if (ev.shiftKey && !select_between_enabled) { + select_between_enabled = true + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + } + + const new_selected = new Set(selected_instance_paths) + + if (select_between_enabled) { + handle_select_between(instance_path, new_selected) + } else if (ev.ctrlKey || multi_select_enabled) { + new_selected.has(instance_path) ? new_selected.delete(instance_path) : new_selected.add(instance_path) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + send_message({ type: 'selection_changed', data: { selected: [...new_selected] } }) + } else { + update_drive_state({ type: 'runtime/selected_instance_paths', message: [instance_path] }) + send_message({ type: 'selection_changed', data: { selected: [instance_path] } }) + } + } + + function handle_select_between (instance_path, new_selected) { + if (!select_between_first_node) { + select_between_first_node = instance_path + } else { + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in the range + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + new_selected.has(node_instance_path) ? new_selected.delete(node_instance_path) : new_selected.add(node_instance_path) + } + + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + } + + // Reset select between mode after second click + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + render_menubar() + } + } + + // Add the clicked entry and all its parents in the default tree + async function expand_entry_path_in_default (target_instance_path) { + console.log('[SEARCH DEBUG] search_expand_into_default called:', { + target_instance_path, + current_mode: mode, + search_query, + previous_mode, + current_search_entry_states: Object.keys(search_entry_states).length, + current_instance_states: Object.keys(instance_states).length + }) + + if (!target_instance_path) { + console.warn('[SEARCH DEBUG] No target_instance_path provided') + return + } + + const parts = target_instance_path.split('|').filter(Boolean) + if (parts.length === 0) { + console.warn('[SEARCH DEBUG] No valid parts found in instance path:', target_instance_path) + return + } + + console.log('[SEARCH DEBUG] Parsed instance path parts:', parts) + + const root_state = get_or_create_state(instance_states, '|/') + root_state.expanded_subs = true + + // Walk from root to target, expanding the path relative to already expanded entries + for (let i = 0; i < parts.length - 1; i++) { + const parent_base = parts[i] + const child_base = parts[i + 1] + const parent_instance_path = parts.slice(0, i + 1).map(p => '|' + p).join('') + const parent_state = get_or_create_state(instance_states, parent_instance_path) + const parent_entry = await db.get(parent_base) + + console.log('[SEARCH DEBUG] Processing parent-child relationship:', { + parent_base, + child_base, + parent_instance_path, + has_parent_entry: !!parent_entry + }) + + if (!parent_entry) continue + if (Array.isArray(parent_entry.subs) && parent_entry.subs.includes(child_base)) { + parent_state.expanded_subs = true + console.log('[SEARCH DEBUG] Expanded subs for:', parent_instance_path) + } + if (Array.isArray(parent_entry.hubs) && parent_entry.hubs.includes(child_base)) { + parent_state.expanded_hubs = true + console.log('[SEARCH DEBUG] Expanded hubs for:', parent_instance_path) + } + } + } + + // expand multiple selected entry in the default tree + async function expand_selected_entries_in_default (selected_paths) { + console.log('[SEARCH DEBUG] expand_selected_entries_in_default called:', { + selected_paths, + current_mode: mode, + search_query, + previous_mode + }) + + if (!Array.isArray(selected_paths) || selected_paths.length === 0) { + console.warn('[SEARCH DEBUG] No valid selected paths provided') + return + } + + // expand foreach selected path + for (const path of selected_paths) { + await expand_entry_path_in_default(path) + } + + console.log('[SEARCH DEBUG] All selected entries expanded in default mode') + } + + // Add the clicked entry and all its parents in the default tree + async function search_expand_into_default (target_instance_path) { + if (!target_instance_path) { + return + } + + handle_search_node_click(target_instance_path) + await expand_entry_path_in_default(target_instance_path) + + console.log('[SEARCH DEBUG] Current mode before switch:', mode) + console.log('[SEARCH DEBUG] Target previous_mode:', previous_mode) + + // Persist selection and expansion state + update_drive_state({ type: 'runtime/selected_instance_paths', message: [target_instance_path] }) + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + search_query = '' + update_drive_state({ type: 'mode/search_query', message: '' }) + + console.log('[SEARCH DEBUG] About to switch from search mode to:', previous_mode) + update_drive_state({ type: 'mode/current_mode', message: previous_mode }) + } + + function handle_confirm (ev, instance_path) { + if (!ev.target) return + const is_checked = ev.target.checked + const new_selected = new Set(selected_instance_paths) + const new_confirmed = new Set(confirmed_instance_paths) + + // use specific logic for mode + if (mode === 'search') { + handle_search_node_click(instance_path) + } else { + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + } + + if (is_checked) { + new_selected.delete(instance_path) + new_confirmed.add(instance_path) + } else { + new_selected.add(instance_path) + new_confirmed.delete(instance_path) + } + + update_drive_state({ type: 'runtime/selected_instance_paths', message: [...new_selected] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [...new_confirmed] }) + } + + async function toggle_subs (instance_path) { + const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_subs + state.expanded_subs = !state.expanded_subs + + // Update view order tracking for the toggled subs + const base_path = instance_path.split('|').pop() + const entry = await db.get(base_path) + + if (entry && Array.isArray(entry.subs)) { + if (was_expanded && recursive_collapse_flag === true) { + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } + } else { + for (const sub_path of entry.subs) { + await toggle_subs_instance(sub_path, instance_path, instance_states, db) + } + } + } + + last_clicked_node = instance_path + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + build_and_render_view(instance_path) + // Set a flag to prevent the subsequent `onbatch` call from causing a render loop. + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'subs_toggled', data: { instance_path, expanded: state.expanded_subs } }) + + async function toggle_subs_instance (sub_path, instance_path, instance_states, db) { + if (was_expanded) { + // Collapsing so + await remove_instances_recursively(sub_path, instance_path, instance_states, db) + } else { + // Expanding so + await add_instances_recursively(sub_path, instance_path, instance_states, db) + } + } + + async function collapse_and_remove_instance (sub_path, instance_path, instance_states, db) { + await collapse_subs_recursively(sub_path, instance_path, instance_states, db) + await remove_instances_recursively(sub_path, instance_path, instance_states, db) + } + } + + async function toggle_hubs (instance_path) { + const state = get_or_create_state(instance_states, instance_path) + const was_expanded = state.expanded_hubs + state.expanded_hubs ? hub_num-- : hub_num++ + state.expanded_hubs = !state.expanded_hubs + + // Update view order tracking for the toggled hubs + const base_path = instance_path.split('|').pop() + const entry = await db.get(base_path) + + if (entry && Array.isArray(entry.hubs)) { + if (was_expanded && recursive_collapse_flag === true) { + // collapse all hub descendants + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } + } else { + // only toggle direct hubs + for (const hub_path of entry.hubs) { + await toggle_hubs_instance(hub_path, instance_path, instance_states, db) + } + } + + async function collapse_and_remove_instance (hub_path, instance_path, instance_states, db) { + await collapse_hubs_recursively(hub_path, instance_path, instance_states, db) + await remove_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + + last_clicked_node = instance_path + drive_updated_by_scroll = true // Prevent onbatch interference with hub spacer + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + build_and_render_view(instance_path, true) + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/instance_states', message: instance_states }) + send_message({ type: 'hubs_toggled', data: { instance_path, expanded: state.expanded_hubs } }) + + async function toggle_hubs_instance (hub_path, instance_path, instance_states, db) { + if (was_expanded) { + // Collapsing so + await remove_instances_recursively(hub_path, instance_path, instance_states, db) + } else { + // Expanding so + await add_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + } + + async function toggle_search_subs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_subs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_subs || false, + recursive_collapse_flag + }) + + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_subs + state.expanded_subs = !state.expanded_subs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = await db.get(base_path) + if (entry && Array.isArray(entry.subs)) entry.subs.forEach(sub_path => collapse_search_subs_recursively(sub_path, instance_path, search_entry_states, db)) + } + + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs ? null : true + const has_matching_parents = manipulated_inside_search[instance_path] ? search_entry_states[instance_path]?.expanded_hubs : search_state_instances[instance_path]?.expanded_hubs + manipulated_inside_search[instance_path] = { expanded_hubs: has_matching_parents, expanded_subs: has_matching_descendant } + console.log('[SEARCH DEBUG] Toggled subs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_subs, + recursive_state: old_expanded && recursive_collapse_flag === true + }) + + handle_search_node_click(instance_path) + + perform_search(search_query) + drive_updated_by_search = true + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) + } + + async function toggle_search_hubs (instance_path) { + console.log('[SEARCH DEBUG] toggle_search_hubs called:', { + instance_path, + mode, + search_query, + current_state: search_entry_states[instance_path]?.expanded_hubs || false, + recursive_collapse_flag + }) + + const state = get_or_create_state(search_entry_states, instance_path) + const old_expanded = state.expanded_hubs + state.expanded_hubs = !state.expanded_hubs + + if (old_expanded && recursive_collapse_flag === true) { + const base_path = instance_path.split('|').pop() + const entry = await db.get(base_path) + if (entry && Array.isArray(entry.hubs)) entry.hubs.forEach(hub_path => collapse_search_hubs_recursively(hub_path, instance_path, search_entry_states, db)) + } + + const has_matching_descendant = search_state_instances[instance_path]?.expanded_subs + manipulated_inside_search[instance_path] = { expanded_hubs: state.expanded_hubs, expanded_subs: has_matching_descendant } + console.log('[SEARCH DEBUG] Toggled hubs state:', { + instance_path, + old_expanded, + new_expanded: state.expanded_hubs, + recursive_state: old_expanded && recursive_collapse_flag === true + }) + + handle_search_node_click(instance_path) + + console.log('[SEARCH DEBUG] About to perform_search after toggle_search_hubs') + perform_search(search_query) + drive_updated_by_search = true + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) + console.log('[SEARCH DEBUG] toggle_search_hubs completed') + } + + function handle_search_node_click (instance_path) { + console.log('[SEARCH DEBUG] handle_search_node_click called:', { + instance_path, + current_mode: mode, + search_query, + previous_last_clicked: last_clicked_node + }) + + if (mode !== 'search') { + console.warn('[SEARCH DEBUG] handle_search_node_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + // we need to handle last_clicked_node differently + const old_last_clicked = last_clicked_node + last_clicked_node = instance_path + + console.log('[SEARCH DEBUG] Updating last_clicked_node:', { + old_value: old_last_clicked, + new_value: last_clicked_node, + mode, + search_query + }) + + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Update visual styling for search mode nodes + update_search_last_clicked_styling(instance_path) + } + + function update_search_last_clicked_styling (target_instance_path) { + console.log('[SEARCH DEBUG] update_search_last_clicked_styling called:', { + target_instance_path, + mode, + search_query + }) + + // Remove `last-clicked` class from all search result nodes + const search_nodes = container.querySelectorAll('.node.search-result') + console.log('[SEARCH DEBUG] Found search result nodes:', search_nodes.length) + search_nodes.forEach(node => remove_last_clicked_styling(node)) + + // Add last-clicked class to the target node if it exists in search results + const target_node = container.querySelector(`[data-instance_path="${target_instance_path}"].search-result`) + if (target_node) { + mode === 'search' ? target_node.classList.add('search-last-clicked') : target_node.classList.add('last-clicked') + console.log('[SEARCH DEBUG] Added last-clicked to target node:', target_instance_path) + } else { + console.warn('[SEARCH DEBUG] Target node not found in search results:', { + target_instance_path, + available_search_nodes: Array.from(search_nodes).map(n => n.dataset.instance_path) + }) + } + + function remove_last_clicked_styling (node) { + const was_last_clicked = node.classList.contains('last-clicked') + mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked') + if (was_last_clicked) { + console.log('[SEARCH DEBUG] Removed last-clicked from:', node.dataset.instance_path) + } + } + } + + function handle_search_name_click (ev, instance_path) { + console.log('[SEARCH DEBUG] handle_search_name_click called:', { + instance_path, + mode, + search_query, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + shiftKey: ev.shiftKey, + multi_select_enabled, + current_selected: selected_instance_paths.length + }) + + if (mode !== 'search') { + console.error('[SEARCH DEBUG] handle_search_name_click called but not in search mode!', { + current_mode: mode, + instance_path + }) + return + } + + handle_search_node_click(instance_path) + + if (ev.ctrlKey || ev.metaKey || multi_select_enabled) { + search_select_node(ev, instance_path) + } else if (ev.shiftKey) { + search_select_node(ev, instance_path) + } else if (select_between_enabled) { + // Handle select-between mode when button is enabled + search_select_node(ev, instance_path) + } else { + // Regular click + search_expand_into_default(instance_path) + } + } + + function search_select_node (ev, instance_path) { + console.log('[SEARCH DEBUG] search_select_node called:', { + instance_path, + mode, + search_query, + shiftKey: ev.shiftKey, + ctrlKey: ev.ctrlKey, + metaKey: ev.metaKey, + multi_select_enabled, + select_between_enabled, + select_between_first_node, + current_selected: selected_instance_paths + }) + + const new_selected = new Set(selected_instance_paths) + + if (select_between_enabled) { + if (!select_between_first_node) { + select_between_first_node = instance_path + console.log('[SEARCH DEBUG] Set first node for select between:', instance_path) + } else { + console.log('[SEARCH DEBUG] Completing select between range:', { + first: select_between_first_node, + second: instance_path + }) + const first_index = view.findIndex(n => n.instance_path === select_between_first_node) + const second_index = view.findIndex(n => n.instance_path === instance_path) + + if (first_index !== -1 && second_index !== -1) { + const start_index = Math.min(first_index, second_index) + const end_index = Math.max(first_index, second_index) + + // Toggle selection for all nodes in between + for (let i = start_index; i <= end_index; i++) { + const node_instance_path = view[i].instance_path + if (new_selected.has(node_instance_path)) { + new_selected.delete(node_instance_path) + } else { + new_selected.add(node_instance_path) + } + } + } + + // Reset select between mode after completing the selection + select_between_enabled = false + select_between_first_node = null + update_drive_state({ type: 'mode/select_between_enabled', message: false }) + render_menubar() + console.log('[SEARCH DEBUG] Reset select between mode') + } + } else if (ev.shiftKey) { + // Enable select between mode on shift click + select_between_enabled = true + select_between_first_node = instance_path + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + console.log('[SEARCH DEBUG] Enabled select between mode with first node:', instance_path) + return + } else if (multi_select_enabled || ev.ctrlKey || ev.metaKey) { + if (new_selected.has(instance_path)) { + console.log('[SEARCH DEBUG] Deselecting node:', instance_path) + new_selected.delete(instance_path) + } else { + console.log('[SEARCH DEBUG] Selecting node:', instance_path) + new_selected.add(instance_path) + } + } else { + // Single selection mode + new_selected.clear() + new_selected.add(instance_path) + console.log('[SEARCH DEBUG] Single selecting node:', instance_path) + } + + const new_selection_array = [...new_selected] + update_drive_state({ type: 'runtime/selected_instance_paths', message: new_selection_array }) + console.log('[SEARCH DEBUG] search_select_node completed, new selection:', new_selection_array) + } + + function reset () { + // reset all of the manual expansions made + instance_states = {} + view_order_tracking = {} // Clear view order tracking on reset + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + if (mode === 'search') { + search_entry_states = {} + drive_updated_by_toggle = true + update_drive_state({ type: 'runtime/search_entry_states', message: search_entry_states }) + perform_search(search_query) + return + } + const root_instance_path = '|/' + const new_instance_states = { + [root_instance_path]: { expanded_subs: true, expanded_hubs: false } + } + update_drive_state({ type: 'runtime/vertical_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/horizontal_scroll_value', message: 0 }) + update_drive_state({ type: 'runtime/selected_instance_paths', message: [] }) + update_drive_state({ type: 'runtime/confirmed_selected', message: [] }) + update_drive_state({ type: 'runtime/instance_states', message: new_instance_states }) + } + + /****************************************************************************** + VIRTUAL SCROLLING + - These functions implement virtual scrolling to handle large graphs + efficiently using an IntersectionObserver. + ******************************************************************************/ + function onscroll () { + if (scroll_update_pending) return + scroll_update_pending = true + requestAnimationFrame(scroll_frames) + function scroll_frames () { + const scroll_delta = vertical_scroll_value - container.scrollTop + // Handle removal of the scroll spacer. + if (spacer_element && scroll_delta > 0 && container.scrollTop === 0) { + spacer_element.remove() + spacer_element = null + spacer_initial_height = 0 + hub_num = 0 + } + + vertical_scroll_value = update_scroll_state({ current_value: vertical_scroll_value, new_value: container.scrollTop, name: 'vertical_scroll_value' }) + horizontal_scroll_value = update_scroll_state({ current_value: horizontal_scroll_value, new_value: container.scrollLeft, name: 'horizontal_scroll_value' }) + scroll_update_pending = false + } + } + + async function fill_viewport_downwards () { + if (is_rendering || end_index >= view.length) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = bottom_sentinel.getBoundingClientRect() + while (end_index < view.length && sentinel_rect.top < container_rect.bottom + 500) { + render_next_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = bottom_sentinel.getBoundingClientRect() + } + is_rendering = false + } + + async function fill_viewport_upwards () { + if (is_rendering || start_index <= 0) return + is_rendering = true + const container_rect = container.getBoundingClientRect() + let sentinel_rect = top_sentinel.getBoundingClientRect() + while (start_index > 0 && sentinel_rect.bottom > container_rect.top - 500) { + render_prev_chunk() + await new Promise(resolve => requestAnimationFrame(resolve)) + sentinel_rect = top_sentinel.getBoundingClientRect() + } + is_rendering = false + } + + function handle_sentinel_intersection (entries) { + entries.forEach(entry => fill_downwards_or_upwards(entry)) + } + + function fill_downwards_or_upwards (entry) { + if (entry.isIntersecting) { + if (entry.target === top_sentinel) fill_viewport_upwards() + else if (entry.target === bottom_sentinel) fill_viewport_downwards() + } + } + + function render_next_chunk () { + if (end_index >= view.length) return + const fragment = document.createDocumentFragment() + const next_end = Math.min(view.length, end_index + chunk_size) + for (let i = end_index; i < next_end; i++) { if (view[i]) fragment.appendChild(create_node(view[i])) } + container.insertBefore(fragment, bottom_sentinel) + end_index = next_end + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + cleanup_dom(false) + } + + function render_prev_chunk () { + if (start_index <= 0) return + const fragment = document.createDocumentFragment() + const prev_start = Math.max(0, start_index - chunk_size) + for (let i = prev_start; i < start_index; i++) { + if (view[i]) fragment.appendChild(create_node(view[i])) + } + container.insertBefore(fragment, top_sentinel.nextSibling) + start_index = prev_start + top_sentinel.style.height = `${start_index * node_height}px` + cleanup_dom(true) + } + + // Removes nodes from the DOM that are far outside the viewport. + function cleanup_dom (is_scrolling_up) { + const rendered_count = end_index - start_index + if (rendered_count <= max_rendered_nodes) return + + const to_remove_count = rendered_count - max_rendered_nodes + if (is_scrolling_up) { + // If scrolling up, remove nodes from the bottom. + remove_dom_nodes({ count: to_remove_count, start_el: bottom_sentinel, next_prop: 'previousElementSibling', boundary_el: top_sentinel }) + end_index -= to_remove_count + bottom_sentinel.style.height = `${(view.length - end_index) * node_height}px` + } else { + // If scrolling down, remove nodes from the top. + remove_dom_nodes({ count: to_remove_count, start_el: top_sentinel, next_prop: 'nextElementSibling', boundary_el: bottom_sentinel }) + start_index += to_remove_count + top_sentinel.style.height = `${start_index * node_height}px` + } + } + + /****************************************************************************** + ENTRY DUPLICATION PREVENTION + ******************************************************************************/ + + function collect_all_duplicate_entries () { + duplicate_entries_map = {} + // Use view_order_tracking for duplicate detection + for (const [base_path, instance_paths] of Object.entries(view_order_tracking)) { + if (instance_paths.length > 1) { + duplicate_entries_map[base_path] = { + instances: instance_paths, + first_instance: instance_paths[0] // First occurrence in view order + } + } + } + } + + async function initialize_tracking_from_current_state () { + const root_path = '/' + const root_instance_path = '|/' + if (await db.has(root_path)) { + add_instance_to_view_tracking(root_path, root_instance_path) + // Add initially expanded subs if any + const root_entry = await db.get(root_path) + if (root_entry && Array.isArray(root_entry.subs)) { + for (const sub_path of root_entry.subs) { + await add_instances_recursively(sub_path, root_instance_path, instance_states, db) + } + } + } + } + + function add_instance_to_view_tracking (base_path, instance_path) { + if (!view_order_tracking[base_path]) view_order_tracking[base_path] = [] + if (!view_order_tracking[base_path].includes(instance_path)) { + view_order_tracking[base_path].push(instance_path) + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } + } + } + + function remove_instance_from_view_tracking (base_path, instance_path) { + if (view_order_tracking[base_path]) { + const index = view_order_tracking[base_path].indexOf(instance_path) + if (index !== -1) { + view_order_tracking[base_path].splice(index, 1) + // Clean up empty arrays + if (view_order_tracking[base_path].length === 0) { + delete view_order_tracking[base_path] + } + + // Only save to drive if not currently loading from drive + if (!is_loading_from_drive) { + drive_updated_by_tracking = true + update_drive_state({ type: 'runtime/view_order_tracking', message: view_order_tracking }) + } + } + } + } + + // Recursively add instances to tracking when expanding + async function add_instances_recursively (base_path, parent_instance_path, instance_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + for (const hub_path of entry.hubs) { + await add_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + for (const sub_path of entry.subs) { + await add_instances_recursively(sub_path, instance_path, instance_states, db) + } + } + + // Add the instance itself + add_instance_to_view_tracking(base_path, instance_path) + } + + // Recursively remove instances from tracking when collapsing + async function remove_instances_recursively (base_path, parent_instance_path, instance_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + for (const hub_path of entry.hubs) { + await remove_instances_recursively(hub_path, instance_path, instance_states, db) + } + } + if (state.expanded_subs && Array.isArray(entry.subs)) { + for (const sub_path of entry.subs) { + await remove_instances_recursively(sub_path, instance_path, instance_states, db) + } + } + + // Remove the instance itself + remove_instance_from_view_tracking(base_path, instance_path) + } + + // Recursively hubs all subs in default mode + async function collapse_subs_recursively (base_path, parent_instance_path, instance_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) // Decrement hub counter + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } + } + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_subs_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) + } + } + + // Recursively hubs all hubs in default mode + async function collapse_hubs_recursively (base_path, parent_instance_path, instance_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance(hub_path, instance_path, instance_states, db) + } + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_and_remove_instance(sub_path, instance_path, instance_states, db) + } + } + async function collapse_and_remove_instance (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) + } + } + + // Recursively collapse in default mode + async function collapse_all_recursively (base_path, parent_instance_path, instance_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(instance_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_and_remove_instance_recursively(sub_path, instance_path, instance_states, db) + } + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + hub_num = Math.max(0, hub_num - 1) + for (const hub_path of entry.hubs) { + await collapse_and_remove_instance_recursively(hub_path, instance_path, instance_states, db) + } + } + + async function collapse_and_remove_instance_recursively (base_path, instance_path, instance_states, db) { + await collapse_all_recursively(base_path, instance_path, instance_states, db) + await remove_instances_recursively(base_path, instance_path, instance_states, db) + } + } + + // Recursively subs all hubs in search mode + async function collapse_search_subs_recursively (base_path, parent_instance_path, search_entry_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } + } + } + + // Recursively hubs all hubs in search mode + async function collapse_search_hubs_recursively (base_path, parent_instance_path, search_entry_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } + } + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } + } + } + + // Recursively collapse in search mode + async function collapse_search_all_recursively (base_path, parent_instance_path, search_entry_states, db) { + const instance_path = `${parent_instance_path}|${base_path}` + const entry = await db.get(base_path) + if (!entry) return + + const state = get_or_create_state(search_entry_states, instance_path) + + if (state.expanded_subs && Array.isArray(entry.subs)) { + state.expanded_subs = false + for (const sub_path of entry.subs) { + await collapse_search_all_recursively(sub_path, instance_path, search_entry_states, db) + } + } + + if (state.expanded_hubs && Array.isArray(entry.hubs)) { + state.expanded_hubs = false + for (const hub_path of entry.hubs) { + await collapse_search_all_recursively(hub_path, instance_path, search_entry_states, db) + } + } + } + + function get_next_duplicate_instance (base_path, current_instance_path) { + const duplicates = duplicate_entries_map[base_path] + if (!duplicates || duplicates.instances.length <= 1) return null + + const current_index = duplicates.instances.indexOf(current_instance_path) + if (current_index === -1) return duplicates.instances[0] + + const next_index = (current_index + 1) % duplicates.instances.length + return duplicates.instances[next_index] + } + + function has_duplicates (base_path) { + return duplicate_entries_map[base_path] && duplicate_entries_map[base_path].instances.length > 1 + } + + function is_first_duplicate (base_path, instance_path) { + const duplicates = duplicate_entries_map[base_path] + return duplicates && duplicates.first_instance === instance_path + } + + function cycle_to_next_duplicate (base_path, current_instance_path) { + const next_instance_path = get_next_duplicate_instance(base_path, current_instance_path) + if (next_instance_path) { + remove_jump_button_from_entry(current_instance_path) + + // First, handle the scroll and DOM updates without drive state changes + scroll_to_and_highlight_instance(next_instance_path, current_instance_path) + + // Manually update DOM styling + update_last_clicked_styling(next_instance_path) + last_clicked_node = next_instance_path + drive_updated_by_scroll = true // Prevent onbatch from interfering with scroll + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: next_instance_path }) + + // Add jump button to the target entry (with a small delay to ensure DOM is ready) + setTimeout(jump_out, 10) + function jump_out () { + const target_element = shadow.querySelector(`[data-instance_path="${CSS.escape(next_instance_path)}"]`) + if (target_element) { + add_jump_button_to_matching_entry(target_element, base_path, next_instance_path) + } + } + } + } + + function update_last_clicked_styling (new_instance_path) { + // Remove last-clicked class from all elements + const all_nodes = mode === 'search' ? shadow.querySelectorAll('.node.search-last-clicked') : shadow.querySelectorAll('.node.last-clicked') + console.log('Removing last-clicked class from all nodes', all_nodes) + all_nodes.forEach(node => (mode === 'search' ? node.classList.remove('search-last-clicked') : node.classList.remove('last-clicked'))) + // Add last-clicked class to the new element + if (new_instance_path) { + const new_element = shadow.querySelector(`[data-instance_path="${CSS.escape(new_instance_path)}"]`) + if (new_element) { + mode === 'search' ? new_element.classList.add('search-last-clicked') : new_element.classList.add('last-clicked') + } + } + } + + function remove_jump_button_from_entry (instance_path) { + const current_element = shadow.querySelector(`[data-instance_path="${CSS.escape(instance_path)}"]`) + if (current_element) { + // restore the wand icon + const node_data = view.find(n => n.instance_path === instance_path) + if (node_data && node_data.base_path === '/' && instance_path === '|/') { + const wand_el = current_element.querySelector('.wand.navigate-to-hub') + if (wand_el && root_wand_state) { + wand_el.textContent = root_wand_state.content + wand_el.className = root_wand_state.className + wand_el.onclick = root_wand_state.onclick + + root_wand_state = null + } + return + } + + // Regular behavior for non-root nodes + const button_container = current_element.querySelector('.indent-btn-container') + if (button_container) { + button_container.remove() + // Restore left-indent class + if (node_data && node_data.depth > 0) { + current_element.classList.add('left-indent') + } + } + } + } + + function add_jump_button_to_matching_entry (el, base_path, instance_path) { + // Check if jump button already exists + if (el.querySelector('.navigate-to-hub')) return + + // replace the wand icon temporarily + if (base_path === '/' && instance_path === '|/') { + const wand_el = el.querySelector('.wand') + if (wand_el) { + // Store original wand state in JavaScript variable + root_wand_state = { + content: wand_el.textContent, + className: wand_el.className, + onclick: wand_el.onclick + } + + // Replace with jump button + wand_el.textContent = '^' + wand_el.className = 'wand navigate-to-hub clickable' + wand_el.onclick = (ev) => handle_jump_button_click(ev, instance_path) + } + return + + function handle_jump_button_click (ev, instance_path) { + ev.stopPropagation() + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) + } + } + + const indent_button_div = document.createElement('div') + indent_button_div.className = 'indent-btn-container' + + const navigate_button = document.createElement('span') + navigate_button.className = 'navigate-to-hub clickable' + navigate_button.textContent = '^' + navigate_button.onclick = (ev) => handle_navigate_button_click(ev, instance_path) + + indent_button_div.appendChild(navigate_button) + + // Remove left padding + el.classList.remove('left-indent') + el.insertBefore(indent_button_div, el.firstChild) + + function handle_navigate_button_click (ev, instance_path) { + ev.stopPropagation() // Prevent triggering the whole entry click again + // Manually update last clicked node for jump button + last_clicked_node = instance_path + drive_updated_by_match = true + update_drive_state({ type: 'runtime/last_clicked_node', message: instance_path }) + + // Manually update DOM classes for last-clicked styling + update_last_clicked_styling(instance_path) + + cycle_to_next_duplicate(base_path, instance_path) + } + } + + function scroll_to_and_highlight_instance (target_instance_path, source_instance_path = null) { + const target_index = view.findIndex(n => n.instance_path === target_instance_path) + if (target_index === -1) return + + // Calculate scroll position + let target_scroll_top = target_index * node_height + + if (source_instance_path) { + const source_index = view.findIndex(n => n.instance_path === source_instance_path) + if (source_index !== -1) { + const source_scroll_top = source_index * node_height + const current_scroll_top = container.scrollTop + const source_visible_offset = source_scroll_top - current_scroll_top + target_scroll_top = target_scroll_top - source_visible_offset + } + } + + container.scrollTop = target_scroll_top + } + + /****************************************************************************** + HELPER FUNCTIONS + ******************************************************************************/ + function get_highlighted_name (name, query) { + // Creates a new regular expression. + // `escape_regex(query)` sanitizes the query string to treat special regex characters literally. + // `(...)` creates a capturing group for the escaped query. + // 'gi' flags: 'g' for global (all occurrences), 'i' for case-insensitive. + const regex = new RegExp(`(${escape_regex(query)})`, 'gi') + // Replaces all matches of the regex in 'name' with the matched text wrapped in search-match class. + // '$1' refers to the content of the first capturing group (the matched query). + return name.replace(regex, '$1') + } + + function escape_regex (string) { + // Escapes special regular expression characters in a string. + // It replaces characters like -, /, \, ^, $, *, +, ?, ., (, ), |, [, ], {, } + // with their escaped versions (e.g., '.' becomes '\.'). + // This prevents them from being interpreted as regex metacharacters. + return string.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') // Corrected: should be \\$& to escape the found char + } + + function check_and_reset_feedback_flags () { + if (drive_updated_by_scroll && !ignore_drive_updated_by_scroll) { + drive_updated_by_scroll = false + return true + } else ignore_drive_updated_by_scroll = false + if (drive_updated_by_toggle) { + drive_updated_by_toggle = false + return true + } + if (drive_updated_by_search) { + drive_updated_by_search = false + return true + } + if (drive_updated_by_match) { + drive_updated_by_match = false + return true + } + if (drive_updated_by_tracking) { + drive_updated_by_tracking = false + return true + } + if (drive_updated_by_last_clicked) { + drive_updated_by_last_clicked = false + return true + } + if (drive_updated_by_undo) { + drive_updated_by_undo = false + return true + } + console.log('[SEARCH DEBUG] No feedback flags set, allowing onbatch') + return false + } + + function parse_json_data (data, path) { + if (data === null) return null + try { + return typeof data === 'string' ? JSON.parse(data) : data + } catch (e) { + console.error(`Failed to parse JSON for ${path}:`, e) + return null + } + } + + function process_path_array_update ({ current_paths, value, render_set, name }) { + const old_paths = [...current_paths] + const new_paths = Array.isArray(value) + ? value + : (console.warn(`${name} is not an array, defaulting to empty.`, value), []) + ;[...new Set([...old_paths, ...new_paths])].forEach(p => render_set.add(p)) + return new_paths + } + + function calculate_new_scroll_top ({ old_scroll_top, old_view, focal_path }) { + // Calculate the new scroll position to maintain the user's viewport. + if (focal_path) { + // If an action was focused on a specific node (like a toggle), try to keep it in the same position. + const old_idx = old_view.findIndex(n => n.instance_path === focal_path) + const new_idx = view.findIndex(n => n.instance_path === focal_path) + if (old_idx !== -1 && new_idx !== -1) { + return old_scroll_top + (new_idx - old_idx) * node_height + } + } else if (old_view.length > 0) { + // Otherwise, try to keep the topmost visible node in the same position. + const old_top_idx = Math.floor(old_scroll_top / node_height) + const old_top_node = old_view[old_top_idx] + if (old_top_node) { + const new_top_idx = view.findIndex(n => n.instance_path === old_top_node.instance_path) + if (new_top_idx !== -1) { + return new_top_idx * node_height + (old_scroll_top % node_height) + } + } + } + return old_scroll_top + } + + function handle_spacer_element ({ hub_toggle, existing_height, new_scroll_top, sync_fn }) { + if (hub_toggle || hub_num > 0) { + spacer_element = document.createElement('div') + spacer_element.className = 'spacer' + container.appendChild(spacer_element) + + if (hub_toggle) { + requestAnimationFrame(spacer_frames) + } else { + spacer_element.style.height = `${existing_height}px` + requestAnimationFrame(sync_fn) + } + } else { + spacer_element = null + spacer_initial_height = 0 + requestAnimationFrame(sync_fn) + } + function spacer_frames () { + const container_height = container.clientHeight + const content_height = view.length * node_height + const max_scroll_top = content_height - container_height + + if (new_scroll_top > max_scroll_top) { + spacer_initial_height = new_scroll_top - max_scroll_top + spacer_element.style.height = `${spacer_initial_height}px` + } + sync_fn() + } + } + + function create_root_node ({ state, has_subs, instance_path }) { + // Handle the special case for the root node since its a bit different. + const el = document.createElement('div') + el.className = 'node type-root' + el.dataset.instance_path = instance_path + const prefix_class = has_subs || (mode === 'search' && search_query) ? 'prefix clickable' : 'prefix' + const prefix_name = state.expanded_subs ? 'tee-down' : 'line-h' + el.innerHTML = `
🪄
/🌐` + + el.querySelector('.wand').onclick = reset + if (has_subs) { + const prefix_el = el.querySelector('.prefix') + if (prefix_el) { + prefix_el.onclick = (mode === 'search' && search_query) ? null : () => toggle_subs(instance_path) + } + } + el.querySelector('.name').onclick = ev => (mode === 'search' && search_query) ? null : select_node(ev, instance_path) + return el + } + + function create_confirm_checkbox (instance_path) { + const checkbox_div = document.createElement('div') + checkbox_div.className = 'confirm-wrapper' + const is_confirmed = confirmed_instance_paths.includes(instance_path) + checkbox_div.innerHTML = `` + const checkbox_input = checkbox_div.querySelector('input') + if (checkbox_input) checkbox_input.onchange = ev => handle_confirm(ev, instance_path) + return checkbox_div + } + + function update_scroll_state ({ current_value, new_value, name }) { + if (current_value !== new_value) { + drive_updated_by_scroll = true // Set flag to prevent render loop. + update_drive_state({ type: `runtime/${name}`, message: new_value }) + return new_value + } + return current_value + } + + function remove_dom_nodes ({ count, start_el, next_prop, boundary_el }) { + for (let i = 0; i < count; i++) { + const temp = start_el[next_prop] + if (temp && temp !== boundary_el) temp.remove() + else break + } + } + + /****************************************************************************** + KEYBOARD NAVIGATION + - Handles keyboard-based navigation for the graph explorer + - Navigate up/down around last_clicked node + ******************************************************************************/ + function handle_keyboard_navigation (event) { + // Don't handle keyboard events if focus is on input elements + if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') { + return + } + const on_bind = { + navigate_up_current_node, + navigate_down_current_node, + toggle_subs_for_current_node, + toggle_hubs_for_current_node, + multiselect_current_node, + select_between_current_node, + toggle_search_mode, + jump_to_next_duplicate + + } + let key_combination = '' + if (event.ctrlKey) key_combination += 'Control+' + if (event.altKey) key_combination += 'Alt+' + if (event.shiftKey) key_combination += 'Shift+' + key_combination += event.key + + const action = keybinds[key_combination] || keybinds[event.key] + if (!action) return + + // Prevent default behavior for handled keys + event.preventDefault() + const base_path = last_clicked_node.split('|').pop() + const current_instance_path = last_clicked_node + // Execute the appropriate action + on_bind[action]({ base_path, current_instance_path }) + } + function navigate_up_current_node () { + navigate_to_adjacent_node(-1) + } + function navigate_down_current_node () { + navigate_to_adjacent_node(1) + } + function navigate_to_adjacent_node (direction) { + if (view.length === 0) return + if (!last_clicked_node) last_clicked_node = view[0].instance_path + const current_index = view.findIndex(node => node.instance_path === last_clicked_node) + if (current_index === -1) return + + const new_index = current_index + direction + if (new_index < 0 || new_index >= view.length) return + + const new_node = view[new_index] + last_clicked_node = new_node.instance_path + drive_updated_by_last_clicked = true + update_drive_state({ type: 'runtime/last_clicked_node', message: last_clicked_node }) + + // Update visual styling + if (mode === 'search' && search_query) { + update_search_last_clicked_styling(last_clicked_node) + } else { + update_last_clicked_styling(last_clicked_node) + } + const base_path = last_clicked_node.split('|').pop() + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) { + const el = shadow.querySelector(`[data-instance_path="${CSS.escape(last_clicked_node)}"]`) + add_jump_button_to_matching_entry(el, base_path, last_clicked_node) + } + scroll_to_node(new_node.instance_path) + } + + async function toggle_subs_for_current_node () { + if (!last_clicked_node) return + + const base_path = last_clicked_node.split('|').pop() + const entry = await db.get(base_path) + const has_subs = Array.isArray(entry?.subs) && entry.subs.length > 0 + if (!has_subs) return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + if (has_duplicate_entries && !is_first_occurrence) return + } + + if (mode === 'search' && search_query) { + await toggle_search_subs(last_clicked_node) + } else { + await toggle_subs(last_clicked_node) + } + } + + async function toggle_hubs_for_current_node () { + if (!last_clicked_node) return + + const base_path = last_clicked_node.split('|').pop() + const entry = await db.get(base_path) + const has_hubs = hubs_flag === 'false' ? false : Array.isArray(entry?.hubs) && entry.hubs.length > 0 + if (!has_hubs || base_path === '/') return + + if (hubs_flag === 'default') { + const has_duplicate_entries = has_duplicates(base_path) + const is_first_occurrence = is_first_duplicate(base_path, last_clicked_node) + + if (has_duplicate_entries && !is_first_occurrence) return + } + + if (mode === 'search' && search_query) { + await toggle_search_hubs(last_clicked_node) + } else { + await toggle_hubs(last_clicked_node) + } + } + + function multiselect_current_node () { + if (!last_clicked_node || selection_flag === false) return + + // IMPORTANT FIX!!!!! : synthetic event object for compatibility with existing functions + const synthetic_event = { ctrlKey: true, metaKey: false, shiftKey: false } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + + function select_between_current_node () { + if (!last_clicked_node || selection_flag === false) return + + if (!select_between_enabled) { + // Enable select between mode and set first node + select_between_enabled = true + select_between_first_node = last_clicked_node + update_drive_state({ type: 'mode/select_between_enabled', message: true }) + render_menubar() + } else { + // Complete the select between operation + const synthetic_event = { ctrlKey: false, metaKey: false, shiftKey: true } + + if (mode === 'search' && search_query) { + search_select_node(synthetic_event, last_clicked_node) + } else { + select_node(synthetic_event, last_clicked_node) + } + } + } + + function scroll_to_node (instance_path) { + const node_index = view.findIndex(node => node.instance_path === instance_path) + if (node_index === -1 || !node_height) return + + const target_scroll_top = node_index * node_height + const container_height = container.clientHeight + const current_scroll_top = container.scrollTop + + // Only scroll if the node is not fully visible + if (target_scroll_top < current_scroll_top || target_scroll_top + node_height > current_scroll_top + container_height) { + const centered_scroll_top = target_scroll_top - (container_height / 2) + (node_height / 2) + container.scrollTop = Math.max(0, centered_scroll_top) + + vertical_scroll_value = container.scrollTop + drive_updated_by_scroll = true + update_drive_state({ type: 'runtime/vertical_scroll_value', message: vertical_scroll_value }) + } + } + + function jump_to_next_duplicate ({ base_path, current_instance_path }) { + if (hubs_flag === 'default') { + cycle_to_next_duplicate(base_path, current_instance_path) + } + } + + /****************************************************************************** + UNDO FUNCTIONALITY + - Implements undo functionality to revert drive state changes + ******************************************************************************/ + async function undo (steps = 1) { + if (undo_stack.length === 0) { + console.warn('No actions to undo') + return + } + + const actions_to_undo = Math.min(steps, undo_stack.length) + console.log(`Undoing ${actions_to_undo} action(s)`) + + // Pop the specified number of actions from the stack + const snapshots_to_restore = [] + for (let i = 0; i < actions_to_undo; i++) { + const snapshot = undo_stack.pop() + if (snapshot) snapshots_to_restore.push(snapshot) + } + + // Restore the last snapshot's state + if (snapshots_to_restore.length > 0) { + const snapshot = snapshots_to_restore[snapshots_to_restore.length - 1] + + try { + // Restore the state WITHOUT setting drive_updated_by_undo flag + // This allows onbatch to process the change and update the UI + await drive.put(`${snapshot.type}.json`, snapshot.value) + + // Update the undo stack in drive (with flag to prevent tracking this update) + // drive_updated_by_undo = true + await drive.put('undo/stack.json', JSON.stringify(undo_stack)) + + console.log(`Undo completed: restored ${snapshot.type} to previous state`) + + // Re-render menubar to update undo button count + render_menubar() + } catch (e) { + console.error('Failed to undo action:', e) + } + } + } +} + +/****************************************************************************** + FALLBACK CONFIGURATION + - This provides the default data and API configuration for the component, + following the pattern described in `instructions.md`. + - It defines the default datasets (`entries`, `style`, `runtime`) and their + initial values. + ******************************************************************************/ +function fallback_module () { + return { + api: fallback_instance + } + function fallback_instance () { + return { + drive: { + 'style/': { + 'theme.css': { + $ref: 'theme.css' + } + }, + 'runtime/': { + 'node_height.json': { raw: '16' }, + 'vertical_scroll_value.json': { raw: '0' }, + 'horizontal_scroll_value.json': { raw: '0' }, + 'selected_instance_paths.json': { raw: '[]' }, + 'confirmed_selected.json': { raw: '[]' }, + 'instance_states.json': { raw: '{}' }, + 'search_entry_states.json': { raw: '{}' }, + 'last_clicked_node.json': { raw: 'null' }, + 'view_order_tracking.json': { raw: '{}' } + }, + 'mode/': { + 'current_mode.json': { raw: '"menubar"' }, + 'previous_mode.json': { raw: '"menubar"' }, + 'search_query.json': { raw: '""' }, + 'multi_select_enabled.json': { raw: 'false' }, + 'select_between_enabled.json': { raw: 'false' } + }, + 'flags/': { + 'hubs.json': { raw: '"default"' }, + 'selection.json': { raw: 'true' }, + 'recursive_collapse.json': { raw: 'true' } + }, + 'keybinds/': { + 'navigation.json': { + raw: JSON.stringify({ + ArrowUp: 'navigate_up_current_node', + ArrowDown: 'navigate_down_current_node', + 'Control+ArrowDown': 'toggle_subs_for_current_node', + 'Control+ArrowUp': 'toggle_hubs_for_current_node', + 'Alt+s': 'multiselect_current_node', + 'Alt+b': 'select_between_current_node', + 'Control+m': 'toggle_search_mode', + 'Alt+j': 'jump_to_next_duplicate' + }) + } + }, + 'undo/': { + 'stack.json': { raw: '[]' } + } + } + } + } +} + +}).call(this)}).call(this,"/node_modules/graph-explorer/lib/graph_explorer.js") +},{"./net_helper":3,"STATE":1}],3:[function(require,module,exports){ +(function (__filename){(function (){ +module.exports = net + +function net(id) { + const [label, _, sub, hub] = [`[${id}@${__filename}]`, {}, {}, {}] + const io = { invite, accept, on: {} } + return { io, _ } + function forward(to, M) { + for (const id of Object.keys(sub)) if (to.startsWith(id)) return sub[id].tx(M) + for (const id of Object.keys(hub)) if (to.startsWith(id)) hub[id].tx(M) + console.error(`[id] ${label} - cant forward to unknown recipient "${to}"`) + } + function invite(name, ids) { + if (!io.on[name]) throw new Error(`${label} no protocol handler for "${name}"`) + return Object.assign(invite, { ids }) + function invite(tx) { + const rx = router(sub) + add(name, tx, tx.id, rx, sub) + return rx + } + } + function accept(invite) { + const rx = router(hub) + const tx = invite(Object.assign(rx, { id })) + for (const [name, to] of Object.entries(invite.ids)) { + if (hub[to]) throw new Error(`${label} already connected to "${to}"`) + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + add(name, tx, to, rx, hub) + } + } + function router($) { + return function rx(M) { + const { head: [by, to, mid] } = M + console.log(`[by] ${by}\n[to] ${to}\n[id]`, M) + if (to !== id) return forward(to, M) + if (!$[by]) throw new Error(`${label} unknown sender "${by}"`) + const { name } = $[by].state + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + io.on[name](M) + } + } + function add(name, tx, to, rx, $) { + if (_[name]) throw new Error(`${label} petname "${name}" is already in use`) + const state = { name, to, mid: 0 } + _[name] = send + $[to] = { rx, tx, state } + function send(type, refs = {}, data = null) { + const head = [id, to, state.mid++] + const meta = { time: Date.now(), stack: (new Error().stack) } + tx({ head, refs, type, data, meta }) + return head + } + } +} + +}).call(this)}).call(this,"/node_modules/graph-explorer/lib/net_helper/net_helper.js") +},{}],4:[function(require,module,exports){ +module.exports = require('ui_gallery') + +},{"ui_gallery":34}],5:[function(require,module,exports){ +(function (global){(function (){ +module.exports = function DEBUG (filename) { + return function (sid) { return create_context(filename, sid) } +} + +const DEFAULT_FLAGS = { + show_default_entries: true +} + +const scope = typeof window !== 'undefined' ? window : global + +if (!scope.__DEBUG_GLOBAL_STATE__) { + scope.__DEBUG_GLOBAL_STATE__ = { + flags: { ...DEFAULT_FLAGS }, + listeners: [] + } +} + +const state = scope.__DEBUG_GLOBAL_STATE__ + +function set_flag (name, value) { + if (state.flags[name] === value) return + state.flags[name] = value + state.listeners.forEach(notify) + + function notify (listener) { listener(name, value) } +} + +function get_flag (name) { return state.flags[name] } + +function get_flags () { return { ...state.flags } } + +function on_change (listener) { + state.listeners.push(listener) + return unsubscribe + + function unsubscribe () { state.listeners = state.listeners.filter(keep) } + function keep (l) { return l !== listener } +} + +function create_context (filename, sid) { + return { + get_flag, + get_flags, + set_flag, + on_change, + meta: { component: filename, sid } + } +} + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],6:[function(require,module,exports){ +(function (global){(function (){ +module.exports = function DOCS (filename) { + return function create_docs (sid) { return create_context(filename, sid) } +} + +const scope = typeof window !== 'undefined' ? window : global + +if (!scope.__DOCS_GLOBAL_STATE__) { + scope.__DOCS_GLOBAL_STATE__ = { + docs_mode_active: false, + docs_mode_listeners: [], + doc_display_callback: null, + action_registry: new Map(), + action_lookup: new Map(), + handler_doc_registry: new Map(), + docs_interaction_state: new WeakMap() + } +} + +const state = scope.__DOCS_GLOBAL_STATE__ +state.action_lookup = state.action_lookup || new Map() +state.handler_doc_registry = state.handler_doc_registry || new Map() +state.docs_interaction_state = state.docs_interaction_state || new WeakMap() + +function set_docs_mode (active) { + if (state.docs_mode_active !== active) state.docs_interaction_state = new WeakMap() + state.docs_mode_active = active + state.docs_mode_listeners.forEach(listener => listener(active)) +} + +function get_docs_mode () { return state.docs_mode_active } + +function on_docs_mode_change (listener) { + state.docs_mode_listeners.push(listener) + return unsubscribe + + function unsubscribe () { + state.docs_mode_listeners = state.docs_mode_listeners.filter(item => item !== listener) + } +} + +function set_doc_display_handler (callback) { state.doc_display_callback = callback } + +function get_actions (sid) { + const actions = state.action_registry.get(sid) + if (!actions) throw new Error('DOCS: No actions registered for SID ' + sid) + return actions +} + +function list_registered () { return Array.from(state.action_registry.keys()) } + +function get_toc (sid) { + return { + actions: state.action_registry.get(sid) || [], + handlers: state.handler_doc_registry.get(sid) || [] + } +} + +function register_handler_doc (meta) { + if (meta.doc === undefined || meta.doc === null) return + const list = state.handler_doc_registry.get(meta.sid) || [] + if (list.some(entry => entry.doc === meta.doc)) return + list.push({ doc: meta.doc, component: meta.component }) + state.handler_doc_registry.set(meta.sid, list) +} + +function clear_handler_docs (sid) { state.handler_doc_registry.delete(sid) } + +function verify_actions (actions) { + if (!Array.isArray(actions)) throw new Error('DOCS: Actions must be array') + actions.forEach(validate_action) + + function validate_action (action, index) { + if (!action.name || typeof action.name !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'name'`) + if (!action.info || typeof action.info !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'info'`) + if (!action.icon || typeof action.icon !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'icon'`) + if (!action.status || typeof action.status !== 'object') throw new Error(`DOCS: Action[${index}] Invalid 'status'`) + if (!action.steps || !Array.isArray(action.steps)) throw new Error(`DOCS: Action[${index}] Invalid 'steps'`) + } +} + +async function display_doc (content, sid) { + let resolved_content = content + if (typeof content === 'function') { + resolved_content = await content() + } else if (content && typeof content.then === 'function') { + resolved_content = await content + } + + if (state.doc_display_callback) { + return state.doc_display_callback({ content: resolved_content || 'No documentation available', sid }) + } +} + +function wrap_isolated (handler, meta) { + if (typeof handler !== 'function') throw new TypeError('DOCS: Isolated handler must be a function') + + const isolated_handler = compile_handler(handler) + const real_state = get_handler_state(handler) + const initial_state = structuredClone(real_state) + + return async function wrapped_handler (event) { + const docs_mode = state.docs_mode_active + if (docs_mode && event) { + if (event.preventDefault) event.preventDefault() + if (event.stopPropagation) event.stopPropagation() + } + + let requested_action + const handler_state = docs_mode ? get_docs_state(real_state, initial_state) : real_state + + function $ (action) { + if (requested_action !== undefined) throw new Error('DOCS: Handler already requested an action') + if (!action || typeof action !== 'string') throw new TypeError('DOCS: Action request must be a name') + requested_action = action + } + $.state = handler_state + + const result = await isolated_handler.call(this, event, $) + if (requested_action !== undefined) return dispatch_action(meta.sid, requested_action, docs_mode) + if (docs_mode) await display_doc(meta.doc || 'No documentation available', meta.sid) + return result + } +} + +function compile_handler (handler) { + // eslint-disable-next-line no-new-func + return new Function(`return (${handler})`)() +} + +function get_handler_state (handler) { + const interaction_state = handler.opts && handler.opts.state + if (interaction_state === undefined) return {} + if (Object.getPrototypeOf(interaction_state) !== Object.prototype) { + throw new TypeError('DOCS: handler.opts.state must be a plain object') + } + structuredClone(interaction_state) + return interaction_state +} + +function get_docs_state (real_state, initial_state) { + let interaction_state = state.docs_interaction_state.get(real_state) + if (!interaction_state) { + interaction_state = structuredClone(initial_state) + state.docs_interaction_state.set(real_state, interaction_state) + } + return interaction_state +} + +function dispatch_action (sid, name, docs_mode) { + const lookup = state.action_lookup.get(sid) + const record = lookup && lookup.get(name) + if (!record) throw new Error(`DOCS: Unknown action "${name}" for SID ${sid}`) + if (docs_mode) return display_doc(record.action.info, sid) + if (!record.run) throw new Error(`DOCS: Action "${record.action.name}" has no run callback`) + return record.run() +} + +function register_actions (sid, actions) { + verify_actions(actions) + const public_actions = [] + const lookup = new Map() + + actions.forEach(register_action) + state.action_registry.set(sid, public_actions) + state.action_lookup.set(sid, lookup) + + function register_action (action) { + const { run, ...public_action } = action + if (run !== undefined && typeof run !== 'function') throw new TypeError(`DOCS: Action "${action.name}" run must be a function`) + + const record = { action: public_action, run } + const keys = new Set([action.name, action.name.toLowerCase().replace(/ /g, '_')]) + keys.forEach(register_key) + if (!action.status.hidden) public_actions.push(public_action) + + function register_key (key) { + if (lookup.has(key)) throw new Error(`DOCS: Duplicate action key "${key}" for SID ${sid}`) + lookup.set(key, record) + } + } +} + +let admin = true +function create_context (filename, sid) { + const api = { + wrap_isolated: wrap_with_component, + get_docs_mode, + on_docs_mode_change, + get_toc: () => get_toc(sid), + clear_handler_docs: () => clear_handler_docs(sid), + register_actions: actions => register_actions(sid, actions) + } + const admin_api = { + set_docs_mode, + set_doc_display_handler, + get_actions, + get_toc, + clear_handler_docs, + list_registered + } + if (admin) { + admin = false + return Object.assign({ admin: admin_api }, api) + } + return api + + function wrap_with_component (handler) { + const meta = { doc: handler && handler.info, sid, component: filename } + register_handler_doc(meta) + return wrap_isolated(handler, meta) + } +} + +}).call(this)}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],7:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +const quick_actions = require('quick_actions') + +module.exports = action_bar + +async function action_bar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: iconject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+ +
+
+ +
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const history_icon = shadow.querySelector('.icon-btn') + const quick_placeholder = shadow.querySelector('quick-actions') + + const { io, _ } = net(id) + let console_icon = {} + const docs = DOCS(__filename)(opts.sid) + const history_action = { + name: 'Toggle Console History', + info: 'Open or close the console history.', + icon: 'console', + status: { hidden: true }, + steps: [], + run: toggle_console_history + } + docs.register_actions([history_action]) + const subs = await sdb.watch(onbatch) + + let selected_action = null + + io.on = { + up: io_up(), + quick_actions: io_quick_actions() + } + if (invite) io.accept(invite) + + history_icon.innerHTML = console_icon + on_history_click.info = history_action.info + history_icon.onclick = docs.wrap_isolated(on_history_click) + const element = await quick_actions({ ...subs[0] }, io.invite('quick_actions', { up: id })) + quick_placeholder.replaceWith(element) + + const parent_handler = { + load_actions, + selected_action: parent_selected_action, + show_submit_btn, + hide_submit_btn, + step_clicked: parent_step_clicked, + update_quick_actions_for_app, + update_quick_actions_input, + action_submitted: parent__action_submitted, + clean_up: parent__clean_up + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('Unknown message type:', type, data) } + function inject ({ data }) { sheet.replaceSync(data[0]) } + function iconject ({ data }) { console_icon = data[0] } + + // ------------------------------- + // Protocol: quick actions + // ------------------------------- + + function io_quick_actions () { + return function quick_actions_protocol (msg) { + const quick_handlers = { + display_actions: quick_actions_display_actions, + action_submitted: quick_actions_action_submitted, + filter_actions: quick_actions_filter_actions, + update_quick_actions_input, + activate_steps_wizard: quick_actions_activate_steps_wizard, + ui_focus_docs + } + + const { type } = msg + const handler = quick_handlers[type] || fail + handler(msg) + } + } + + function quick_actions_filter_actions (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function quick_actions_display_actions (msg) { + const { data } = msg + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + const display = typeof data === 'string' ? data : data.display + const reason = typeof data === 'string' ? '' : data.reason + const should_clean = display === 'none' && reason !== 'selected' + if (should_clean) { + _.up('clean_up', msg.head ? { cause: msg.head } : {}, selected_action) + } + } + + function quick_actions_action_submitted (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'completed' }) + _.up('action_submitted', msg.head ? { cause: msg.head } : {}, { selected_action }) + } + + function io_up () { + return function onmessage (msg) { + const { type } = msg + if (type === 'docs_toggle') { + _.quick_actions(type, msg.head ? { cause: msg.head } : {}, msg.data) + } else { + const handler = parent_handler[type] || fail + handler(msg) + } + } + } + + function load_actions (msg) { + // const { data } = msg + } + function parent_selected_action (msg) { + _.quick_actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function show_submit_btn (msg) { _.quick_actions('show_submit_btn', msg.head ? { cause: msg.head } : {}, null) } + function hide_submit_btn (msg) { _.quick_actions('hide_submit_btn', msg.head ? { cause: msg.head } : {}, null) } + + function update_quick_actions_for_app (msg) { + const { data, type } = msg + _.quick_actions(type, msg.head ? { cause: msg.head } : {}, data) + } + + function update_quick_actions_input (msg) { + const { data } = msg + selected_action = data || null + _.quick_actions('update_input_command', msg.head ? { cause: msg.head } : {}, data) + } + + function quick_actions_activate_steps_wizard (msg) { + _.up('activate_steps_wizard', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function parent_step_clicked (msg) { + const { data } = msg + _.quick_actions('update_current_step', msg.head ? { cause: msg.head } : {}, data) + _.up('render_form', msg.head ? { cause: msg.head } : {}, data) + } + + function parent__action_submitted (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'completed' }) + _.up('action_submitted', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function parent__clean_up (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'cancel' }) + _.up('clean_up', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function ui_focus_docs (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function on_history_click (event, $) { $('Toggle Console History') } + function toggle_console_history () { _.up('console_history_toggle', {}, null) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + quick_actions: { $: '' }, + DOCS: { $: '' }, + net_helper: { $: '' } + } + } + function fallback_instance () { + return { + _: { + quick_actions: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + }, + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + 'console.svg': { + $ref: 'console.svg' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .container { + display: flex; + flex-direction: column; + width: 100%; + } + .action-bar-container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + background: #131315; + padding: 8px; + gap: 12px; + } + .command-history { + display: flex; + align-items: center; + } + .quick-actions { + display: flex; + flex: auto; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + min-width: 340px; + } + .hide { + display: none; + } + + .icon-btn { + display: flex; + min-width: 32px; + height: 32px; + border: none; + background: transparent; + cursor: pointer; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px; + border-radius: 6px; + color: #a6a6a6; + } + .icon-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + svg { + width: 20px; + height: 20px; + } + ` + } + }, + 'actions/': {}, + 'hardcons/': {}, + 'prefs/': {}, + 'variables/': {} + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/action_bar/action_bar.js") +},{"DOCS":6,"STATE":1,"net_helper":20,"quick_actions":23}],8:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const program = require('program') +const steps_wizard = require('steps_wizard') + +const { form_input, input_test, form_tile_split_choice, form_click_rate_test } = program + +const component_modules = { + form_input, + input_test, + form_tile_split_choice, + form_click_rate_test + // Add more form input components here if needed +} + +module.exports = action_executor + +async function action_executor (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ + + +
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const program_placeholder = shadow.querySelector('program') + const form_input_placeholder = shadow.querySelector('form-input') + const steps_wizard_placeholder = shadow.querySelector('steps-wizard') + const { io, _ } = net(id) + + const subs = await sdb.watch(onbatch) + + let all_data = null + let selected_action = null + + io.on = { + up: io_up(), + program: io_program(), + steps_wizard: io_steps_wizard() + } + + // dynamic form input component SIDs + for (const [component_name] of Object.entries(component_modules)) { + // const final_index = index + 2 + io.on[component_name] = io_form_input(component_name) + } + if (invite) io.accept(invite) + + const program_el = await program({ ...subs[0] }, io.invite('program', { up: id })) + program_el.classList.add('program-bar', 'hide') + program_placeholder.replaceWith(program_el) + + const steps_wizard_el = await steps_wizard({ ...subs[1] }, io.invite('steps_wizard', { up: id })) + steps_wizard_el.classList.add('steps-wizard-bar', 'hide') + steps_wizard_placeholder.replaceWith(steps_wizard_el) + + const form_input_elements = {} + + for (const [index, [component_name, component_fn]] of Object.entries(component_modules).entries()) { + const final_index = index + 2 + const sub_entry = subs[final_index] || { sid: opts.sid } + const element = await component_fn({ ...sub_entry }, io.invite(component_name, { up: id })) + element.classList.add('form-inputs', 'hide') + form_input_elements[component_name] = element + form_input_placeholder.parentNode.insertBefore(element, form_input_placeholder) + } + + form_input_placeholder.remove() + + const parent_handler = { + update_steps_wizard_for_app, + load_actions, + action_submitted, + update_data, + activate_steps_wizard, + form_data, + render_form, + selected_action: parent_selected_action, + clean_up + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('Unknown message type:', type, data) } + function inject (data) { sheet.replaceSync(data[0]) } + + // --- Toggle Views --- + function toggle_view (el, show) { el.classList.toggle('hide', !show) } + function steps_toggle_view (display) { toggle_view(steps_wizard_el, display === 'block') } + + function render_form_component (component_name) { + for (const name in form_input_elements) { + toggle_view(form_input_elements[name], name === component_name) + } + } + + function hide_all_forms () { + for (const name in form_input_elements) { + toggle_view(form_input_elements[name], false) + } + } + + // ------------------------------- + // Protocol: program + // ------------------------------- + + function io_program () { + return function program_protocol (msg) { + const program_handlers = { + load_actions: program_load_actions + } + const { type, data } = msg + const handler = program_handlers[type] || fail + handler(data, type, msg) + } + } + + function program_load_actions (data, type, msg) { + _.up(type, msg.head ? { cause: msg.head } : {}, data) + } + + // ------------------------------- + // Protocol: steps wizard + // ------------------------------- + + function io_steps_wizard () { + return function steps_wizard_protocol (msg) { + const steps_handlers = { + step_clicked: steps_wizard_step_clicked + } + + const { type } = msg + const handler = steps_handlers[type] + if (handler) handler(msg) + else _.up(type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } + + function steps_wizard_step_clicked (msg) { + const { data } = msg + const refs = msg.head ? { cause: msg.head } : {} + _.up('step_clicked', refs, data) + + if (should_execute_step(data)) { + _.up('execute_step', refs, { + action: selected_action, + step: data, + commands: data.commands + }) + } + + function should_execute_step (step_data) { + return step_data && Array.isArray(step_data.commands) && step_data.commands.length > 0 + } + } + + // ------------------------------- + // Protocol: form input + // ------------------------------- + + function io_form_input (component_name) { + return function form_input_protocol (msg) { + const form_input_handlers = { + action_submitted: form_action_submitted, + action_incomplete: form_action_incomplete, + action_complete: form__action_complete + } + const { type, data } = msg + const handler = form_input_handlers[type] || fail + handler(data, type, msg) + } + } + + function form_action_submitted (data, type, msg) { + console.error('action_executor: form_action_submitted', data, 'selected_action:', selected_action) + const step = selected_action.steps[data?.index] + Object.assign(step, { + is_completed: true, + status: 'completed', + data: data.value + }) + console.error('action_executor: step updated', step) + + const refs = msg.head ? { cause: msg.head } : {} + _.program('update_data', refs, all_data) + _.steps_wizard('init_data', refs, selected_action.steps) + + if (selected_action.steps[selected_action.steps.length - 1]?.is_completed) { + _.up('show_submit_btn', refs, null) + } + } + + function form_action_incomplete (data, type, msg) { + console.error('action_executor: form_action_incomplete', data) + const step = selected_action.steps[data?.index] + + if (!step.is_completed) return + + Object.assign(step, { + is_completed: false, + status: 'error', + data: data.value !== undefined ? data.value : undefined + }) + const refs = msg.head ? { cause: msg.head } : {} + _.program('update_data', refs, all_data) + _.steps_wizard('init_data', refs, selected_action.steps) + _.up('hide_submit_btn', refs, null) + } + + function form__action_complete (data, type, msg) { + console.error('action_executor: form__action_complete', data, 'selected_action:', selected_action) + if (!selected_action || !selected_action.steps) { + console.error('action_executor: no selected_action or steps') + return + } + + const all_mandatory_complete = selected_action.steps.every(is_step_complete_or_optional) + console.error('action_executor: all_mandatory_complete:', all_mandatory_complete) + + if (all_mandatory_complete) { + hide_all_forms() + _.up('action_auto_completed', msg.head ? { cause: msg.head } : {}, { selected_action, trigger: 'form' }) + } + + function is_step_complete_or_optional (step) { + return step.is_completed || step.type === 'optional' + } + } + + // ------------------------------- + // onmessage from parent + // ------------------------------- + + function io_up () { + return function onmessage (msg) { + const { type } = msg + if (type === 'docs_toggle') { + _.steps_wizard(type, msg.head ? { cause: msg.head } : {}, msg.data) + for (const name in component_modules) { + _[name](type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } else { + parent_handler[type](msg) + } + } + } + + function update_steps_wizard_for_app (msg) { + const { data } = msg + all_data = data + } + + function load_actions (msg) { + _.program(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function action_submitted (msg) { + const { data } = msg + data.result = JSON.stringify(selected_action.steps.map(step => step.data), null, 2) + + reset_selected_action_steps() + _.program('display_result', msg.head ? { cause: msg.head } : {}, data) + } + + function reset_selected_action_steps () { + if (!selected_action?.steps) return + + selected_action.steps.forEach(step => { + step.data = '' + step.is_completed = false + }) + } + + function render_form (msg) { + const { data } = msg + render_form_component(data.component) + const send = _[data.component] + if (send) { + send('step_data', msg.head ? { cause: msg.head } : {}, data) + } + } + + function parent_selected_action (msg) { selected_action = msg.data } + + function update_data (msg) { + const { data: msg_data, type } = msg + _.program(type, msg.head ? { cause: msg.head } : {}, msg_data) + } + + function activate_steps_wizard (msg) { + if (!all_data) return + const steps_data = all_data.find(matches_selected_action) + selected_action = steps_data + if (!steps_data) return + steps_toggle_view('block') + const data = steps_data.steps + _.steps_wizard('init_data', msg.head ? { cause: msg.head } : {}, data) + + function matches_selected_action (action) { + const target = typeof msg.data === 'string' ? msg.data : msg.data?.name + return action.name === target + } + } + + function form_data (msg) { + // forward init_data to steps_wizard with current action steps + _.steps_wizard('init_data', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function clean_up (msg) { + steps_toggle_view('none') + for (const el of Object.values(form_input_elements)) { + toggle_view(el, false) + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + program: { $: '' }, + steps_wizard: { $: '' }, + net_helper: { $: '' } + } + } + function fallback_instance () { + return { + _: { + program: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs' + } + }, + steps_wizard: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs' + } + }, + 'program>form_input': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>input_test': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>form_tile_split_choice': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>form_click_rate_test': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'action_executor.css': { + raw: ` + .container { + display: flex; + flex-direction: column; + width: 100%; + } + .program-bar { + display: flex; + } + .form-inputs { + display: flex; + } + .steps-wizard-bar { + display: flex; + } + .hide { + display: none; + } + ` + } + }, + 'variables/': {}, + 'data/': {}, + 'docs/': {} + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/action_executor/action_executor.js") +},{"STATE":1,"net_helper":20,"program":21,"steps_wizard":25}],9:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = actions + +async function actions (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + actions: onactions, + icons: iconject, + hardcons: onhardcons + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const actions_menu = shadow.querySelector('.actions-menu') + + let init = false + let actions = [] + let icons = {} + let hardcons = {} + const docs = DOCS(__filename)(opts.sid) + const on_message = { + filter_actions: handle_filter_actions, + send_selected_action: handle_send_selected_action, + load_actions: handle_load_actions_message, + update_actions_for_app: handle_update_actions_for_app_message + } + const { io, _ } = net(id) + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_filter_actions (msg) { filter(msg.data) } + function handle_send_selected_action (msg) { + _.up('selected_action', msg.head ? { cause: msg.head } : {}, msg.data) + } + function handle_load_actions_message (msg) { handle_load_actions(msg.data) } + function handle_update_actions_for_app_message (msg) { update_actions_for_app(msg.data) } + function onmessage_fail (msg) { fail(msg.data, msg.type) } + function handle_load_actions (data) { + const converted_actions = Object.keys(data).map(convert_action_key) + actions = converted_actions + if (actions.length > 0) register_actions() + create_actions_menu() + + function convert_action_key (action_key) { + return { + name: action_key, + info: 'Run the ' + action_key + ' action.', + icon: 'file', + status: { pinned: false, default: true }, + steps: [] + } + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + create_actions_menu() + init = true + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function iconject (data) { icons = data } + + function onhardcons (data) { + console.log('Hardcons data:', opts.sid, data) + hardcons = { + pin: data[0], + unpin: data[1], + default: data[2], + undefault: data[3] + } + } + + function onactions (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + actions = vars + if (actions.length > 0) register_actions() + create_actions_menu() + } + + function create_actions_menu () { + actions_menu.replaceChildren() + actions.forEach(create_action_item) + } + + function create_action_item (action_data, index) { + const action_item = document.createElement('div') + action_item.classList.add('action-item') + + const this_icon = icons[index] || icons[0] + action_item.innerHTML = ` +
${this_icon}
+
${action_data.name}
+
${action_data.status && action_data.status.pinned ? hardcons.pin : hardcons.unpin}
+
${action_data.status && action_data.status.default ? hardcons.default : hardcons.undefault}
` + on_action_click.info = action_data.info + on_action_click.opts = { state: { name: action_data.name } } + action_item.onclick = docs.wrap_isolated(on_action_click) + actions_menu.appendChild(action_item) + + function on_action_click (event, $) { $($.state.name) } + } + + function register_actions () { + docs.register_actions(actions.map(bind_action)) + + function bind_action (action) { + return { ...action, run: run_action } + + function run_action () { _.up('selected_action', {}, action) } + } + } + + function filter (search_term) { + const items = shadow.querySelectorAll('.action-item') + items.forEach(update_item_visibility) + + function update_item_visibility (item) { + const action_name = item.children[1].textContent.toLowerCase() + const matches = action_name.includes(search_term.toLowerCase()) + item.style.display = matches ? 'flex' : 'none' + } + } + + async function update_actions_for_app (data) { + if (data) { + drive.put('actions/commands.json', data) + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'actions/': { + 'commands.json': { + raw: JSON.stringify([]) + } + }, + 'icons/': { + 'file.svg': { + $ref: 'icon.svg' + }, + 'folder.svg': { + $ref: 'icon.svg' + }, + 'save.svg': { + $ref: 'icon.svg' + }, + 'gear.svg': { + $ref: 'icon.svg' + }, + 'help.svg': { + $ref: 'icon.svg' + }, + 'terminal.svg': { + $ref: 'icon.svg' + }, + 'search.svg': { + $ref: 'icon.svg' + } + }, + 'hardcons/': { + 'pin.svg': { + $ref: 'pin.svg' + }, + 'unpin.svg': { + $ref: 'unpin.svg' + }, + 'default.svg': { + $ref: 'default.svg' + }, + 'undefault.svg': { + $ref: 'undefault.svg' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .actions-container { + position: relative; + background: #202124; + border: 1px solid #3c3c3c; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + height: auto; + max-height: 100%; + min-height: 0; + width: 100%; + box-sizing: border-box; + overflow-y: auto; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #3c3c3c transparent; + color: #e8eaed; + } + + .actions-container::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + .actions-container::-webkit-scrollbar-track { + background: transparent; + } + + .actions-container::-webkit-scrollbar-thumb { + background: #3c3c3c; + border: 2px solid transparent; + border-radius: 999px; + background-clip: content-box; + } + + .actions-container::-webkit-scrollbar-thumb:hover { + background: #5f6368; + border: 2px solid transparent; + background-clip: content-box; + } + + .actions-menu { + padding: 8px 0; + } + + .action-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + cursor: pointer; + border-bottom: 1px solid #3c3c3c; + transition: background-color 0.2s ease; + } + + .action-item:hover { + background-color: #2d2f31; + } + + .action-item:last-child { + border-bottom: none; + } + + .action-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #a6a6a6; + } + + .action-name { + flex: 1; + font-size: 14px; + color: #e8eaed; + } + + .action-pin .action-default{ + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 12px; + opacity: 0.7; + color: #a6a6a6; + } + + svg { + width: 16px; + height: 16px; + } + ` + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/actions/actions.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],10:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const DEBUG = require('DEBUG') +const net = require('net_helper') + +module.exports = console_history + +async function console_history (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + commands: oncommands, + icons: iconject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const commands_list = shadow.querySelector('.commands-list') + const search_input = shadow.querySelector('.console-search-input') + const clear_btn = shadow.querySelector('.console-search-clear') + const filter_btn = shadow.querySelector('.console-search-filter') + search_input.oninput = on_search_input + clear_btn.onclick = on_clear_search + filter_btn.onclick = on_toggle_default_entries + + let default_commands = [] + const live_commands = [] + let search_term = '' + let dricons = [] + let docs_actions = [] + const docs = DOCS(__filename)(opts.sid) + const debug = DEBUG(__filename)(opts.sid) + const { io, _ } = net(id) + debug.on_change(on_debug_change) + + // Register actions with DOCS system + const actions_file = await drive.get('actions/commands.json') + if (actions_file.raw) { + docs_actions = typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw + docs.register_actions(docs_actions) + } else { + console.error('actions.json not found') + } + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + return el + + function io_up () { + const on_message = { + record_closed_tab: handle_record_closed_tab + } + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + function handle_record_closed_tab (msg) { record_closed_tab(msg.data) } + function onmessage_fail (msg) { console.warn('console_history: unknown message', { cause: msg && msg.type }) } + } + + function record_closed_tab (data) { + const tab = data || {} + const entry = { + icon_type: 'file', + name_path: tab.name || tab.id || 'Tab', + label: 'closed', + pending: true, + status: null, + restore_data: { id: tab.id, name: tab.name } + } + live_commands.unshift(entry) + render_commands() + } + + function resolve_closed_tab (entry, status, do_restore) { + entry.pending = false + entry.status = status + entry.label = status + if (do_restore) _.up('restore_tab', {}, entry.restore_data || { name: entry.name_path }) + render_commands() + } + + function on_search_input (e) { + search_term = e.target.value + render_commands() + } + + function on_clear_search () { + search_term = '' + search_input.value = '' + render_commands() + } + + function on_toggle_default_entries () { + debug.set_flag('show_default_entries', !debug.get_flag('show_default_entries')) + } + + function on_debug_change () { render_commands() } + + function create_command_item (command_data, action) { + const command_el = document.createElement('div') + command_el.className = 'command-item' + + const icon_html = dricons[command_data.icon_type] || dricons.file || '' + const is_pending = !!command_data.pending + const status = command_data.status + + let right_html = '' + if (is_pending) { + right_html = + '
' + (dricons.restore || '') + '
' + + '
' + (dricons.delete || '') + '
' + } else if (status) { + const status_class = String(status).toLowerCase().indexOf('delet') === 0 ? 'deleted' : 'restored' + right_html = '' + status + '' + } + + command_el.innerHTML = ` +
+
${icon_html}
+
+
${command_data.name_path}
+
${command_data.label || ''}
+
+ ${right_html ? `
${right_html}
` : ''} +
` + + on_command_click.info = action.info + on_command_click.opts = { state: { action: action.name } } + command_el.onclick = docs.wrap_isolated(on_command_click) + + const restore_el = command_el.querySelector('.restore-action') + const delete_el = command_el.querySelector('.delete-action') + if (restore_el) restore_el.onclick = on_restore_click + if (delete_el) delete_el.onclick = on_delete_click + + function on_restore_click (e) { + e.stopPropagation() + resolve_closed_tab(command_data, 'Restored', true) + } + + function on_delete_click (e) { + e.stopPropagation() + resolve_closed_tab(command_data, 'Deleted', false) + } + + function on_command_click (event, $) { $($.state.action) } + + return command_el + } + function render_commands () { + commands_list.replaceChildren() + const show_defaults = debug.get_flag('show_default_entries') + const base = show_defaults ? live_commands.concat(default_commands) : live_commands.slice() + const term = search_term.trim().toLowerCase() + const visible = term ? base.filter(matches_search) : base + const actions = visible.map(create_command_action) + docs.register_actions(docs_actions.concat(actions)) + visible.forEach(append_command_item) + + function matches_search (command) { + const haystack = [command.name_path, command.label, command.status] + .filter(Boolean) + .join(' ') + .toLowerCase() + return haystack.includes(term) + } + + function create_command_action (command, index) { + const name = 'Select History Entry ' + (index + 1) + return create_action(name, 'Select ' + command.name_path + ' in command history.', select_command) + + function select_command () { + const command_el = commands_list.children[index] + const previous = commands_list.querySelector('.command-item.selected') + if (previous) previous.classList.remove('selected') + command_el.classList.add('selected') + _.up('ui_focus', {}, { type: 'command_history', sid: opts.sid }) + _.up('command_clicked', {}, command) + } + } + + function append_command_item (command, index) { + commands_list.appendChild(create_command_item(command, actions[index])) + } + } + function create_action (name, info, run) { + return { name, info, icon: 'history', status: { hidden: true }, steps: [], run } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + render_commands() + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function oncommands (data) { + const commands_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + default_commands = Array.isArray(commands_data) ? commands_data.map(normalize_default) : [] + } + + function normalize_default (entry) { + return { + icon_type: entry.icon_type || 'file', + name_path: entry.name_path || entry.name || 'entry', + label: entry.label || entry.status || entry.command || '', + pending: !!entry.pending, + status: entry.pending ? null : (entry.status || null), + restore_data: entry.restore_data || { name: entry.name_path } + } + } + + function iconject (data) { + dricons = { + file: data[0] || '', + bulb: data[1] || '', + restore: data[2] || '', + delete: data[3] || '' + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + DEBUG: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + DEBUG: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'commands/': { + 'list.json': { + $ref: 'commands.json' + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'Clear History', + info: 'Clear the stored console history after confirmation.', + icon: 'trash', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Confirm Clear', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Export History', + info: 'Export console history to the selected format and location.', + icon: 'download', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Choose Format', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Select Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Search History', + info: 'Search through recorded console history entries.', + icon: 'search', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Enter Search Term', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'icons/': { + 'file.svg': { + raw: ` + + + ` + }, + 'bulb.svg': { + raw: ` + + + ` + }, + 'restore.svg': { + raw: ` + + + ` + }, + 'delete.svg': { + raw: ` + + ` + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .console-history-container { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + height: 100%; + background: #202124; + border: 1px solid #3c3c3c; + box-sizing: border-box; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + z-index: 1; + overflow: hidden; + color: #e8eaed; + } + + .console-menu { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + padding: 0px; + } + + /* Sticky search bar pinned to the bottom; scrolling the list above + it does not move it. */ + .console-search { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #18191b; + border-top: 1px solid #3c3c3c; + } + + .console-search-icon { + display: flex; + align-items: center; + justify-content: center; + color: #969ba1; + flex: 0 0 auto; + } + + .console-search-input { + flex: 1 1 auto; + min-width: 0; + box-sizing: border-box; + padding: 6px 8px; + background: transparent; + color: #e8eaed; + border: none; + outline: none; + font-size: 13px; + } + + .console-search-input::placeholder { color: #6b7077; } + + .console-search-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + background: transparent; + border: none; + color: #969ba1; + cursor: pointer; + border-radius: 4px; + } + + .console-search-btn:hover { + color: #e8eaed; + background: rgba(255, 255, 255, 0.08); + } + + .commands-list { + display: flex; + flex-direction: column; + gap: 0px; + } + + .command-item { + display: flex; + align-items: center; + padding: 8px 14px; + background: transparent; + border-bottom: 1px solid #3c3c3c; + cursor: pointer; + transition: background-color 0.15s ease; + } + + .command-item:last-child { + border-bottom: none; + } + + .command-item:hover { + background: #282a2d; + } + + .command-item.selected { + background: #f56300; + } + + .command-item.selected .command-name, + .command-item.selected .command-label, + .command-item.selected .status-text, + .command-item.selected .action-icon { + color: #fff; + } + + .command-content { + display: flex; + align-items: center; + width: 100%; + gap: 12px; + } + + .command-icon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: #2b2c2f; + color: #c8ccd2; + flex: 0 0 auto; + } + + .command-item.selected .command-icon { + background: rgba(255, 255, 255, 0.18); + color: #fff; + } + + .command-icon svg { + width: 16px; + height: 16px; + } + + .command-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1 1 auto; + } + + .command-label { + font-size: 11px; + color: #969ba1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .status-text { + font-size: 12px; + white-space: nowrap; + } + + .status-text.status-restored { color: #46c95d; } + .status-text.status-deleted { color: #ff6b6b; } + + .console-menu::-webkit-scrollbar { + width: 10px; + } + .console-menu::-webkit-scrollbar-track { + background: transparent; + } + .console-menu::-webkit-scrollbar-thumb { + background: #30363d; + border-radius: 999px; + background-clip: content-box; + border: 2px solid transparent; + } + .console-menu::-webkit-scrollbar-thumb:hover { + background: #484f58; + } + .console-menu { + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + } + .command-name { + font-size: 13px; + font-weight: 400; + color: #e8eaed; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .command-path { + font-size: 13px; + color: #969ba1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .command-separator { + color: #969ba1; + margin: 0 4px; + font-size: 13px; + } + + .linked-info { + display: flex; + align-items: center; + gap: 6px; + flex-grow: 1; /* Allow info to take available space */ + + } + + .linked-icon { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: #fbbc04; + } + + .linked-icon svg { + width: 14px; + height: 14px; + } + + .linked-name { + font-size: 13px; + color: #fbbc04; + font-weight: 400; + white-space: nowrap; + } + + .command-actions { + display: flex; + align-items: center; + gap: 10px; /* Adjusted gap */ + margin-left: auto; /* Pushes actions to the right */ + } + + .action-text { + font-size: 13px; + color: #969ba1; + white-space: nowrap; + } + + .action-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #969ba1; + cursor: pointer; + } + + .action-icon:hover { + color: #e8eaed; + } + + .action-icon svg { + width: 16px; + height: 16px; + } + ` + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/console_history/console_history.js") +},{"DEBUG":5,"DOCS":6,"STATE":1,"net_helper":20}],11:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +module.exports = docs_window + +async function docs_window (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ +
+
No documentation available
+
+
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const close_btn = shadow.querySelector('.close-btn') + const docs_text = shadow.querySelector('.docs-text') + + close_btn.onclick = onclose + + await sdb.watch(onbatch) + + return el + + function onclose () { + _.up('close_docs', {}, null) + } + + function io_up () { + return function onmessage (msg) { + const { type, data } = msg + if (type === 'display_doc') { + display_content(data) + } + } + } + + function display_content (data) { + const content = data.content || undefined + docs_text.textContent = content || 'No documentation available' + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} + +function fallback_module () { + return { + _: { + net_helper: { + $: '' + } + }, + api: fallback_instance + } + + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .docs-window { + position: relative; + background: #1e1e2e; + border: 1px solid #3c3c3c; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + color: #e8eaed; + overflow: hidden; + display: flex; + justify-content: space-between; + flex-direction: row-reverse; + flex-wrap: nowrap; + align-items: flex-start; + } + .close-btn { + background: transparent; + border: none; + color: #a6a6a6; + cursor: pointer; + font-size: 16px; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.2s, color 0.2s; + } + .close-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #e8eaed; + } + .docs-content { + padding: 16px; + max-height: 200px; + overflow-y: auto; + } + .docs-text { + font-size: 13px; + line-height: 1.6; + color: #c9d1d9; + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + } + ` + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/docs_window/docs_window.js") +},{"STATE":1,"net_helper":20}],12:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_click_rate_test + +async function form_click_rate_test (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + const click_state = { + accessible: true, + step_index: 0, + count: 0, + start: 0, + complete: false + } + const milliseconds_action = { + name: 'Click Rate Result', + info: 'Calculate and submit the milliseconds taken to complete 10 clicks.', + icon: 'timer', + status: { + pinned: false, + default: false + }, + steps: [], + run: run_milliseconds_action + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
Click Rate Test
+ +
Click as fast as you can. The 10th click completes the action.
+
+
+ + ` + const style = shadow.querySelector('style') + const button = shadow.querySelector('.click-btn') + const count_el = shadow.querySelector('.count') + const result_el = shadow.querySelector('.result') + + on_click.info = 'Record one click in the 10-click sequence. This event is not an action until the 10th click.' + on_click.opts = { state: click_state } + button.onclick = docs.wrap_isolated(on_click) + docs.register_actions([milliseconds_action]) + docs.on_docs_mode_change(on_docs_mode_change) + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { + style.replaceChildren(create_style_element()) + + function create_style_element () { + const style_el = document.createElement('style') + style_el.textContent = data[0] + return style_el + } + } + + function ondata (data) { + if (data.length === 0 || !data[0]) return + const result = data[0].result || '' + click_state.count = result ? data[0].click_count || 10 : 0 + click_state.start = 0 + click_state.complete = Boolean(result) + update_count() + show_result(result) + } + + function step_data (data) { + click_state.accessible = data.is_accessible !== false + click_state.step_index = data.index !== undefined ? data.index : 0 + button.disabled = !click_state.accessible + } + + function reset_data () { + reset_clicks() + drive.put('data/form_click_rate_test.json', { + click_count: 0, + result: '' + }) + } + + function on_docs_mode_change (active) { + if (!active) update_count() + } + + function on_click (event, $) { + if (!$.state.accessible || $.state.complete) return + if ($.state.count === 0) $.state.start = Date.now() + + $.state.count += 1 + event.currentTarget.querySelector('.count').textContent = $.state.count + + if ($.state.count === 10) { + $.state.complete = true + $('click_rate_result') + } + } + + async function run_milliseconds_action () { + const result = '10 clicks in ' + (Date.now() - click_state.start) + ' ms' + show_result(result) + await drive.put('data/form_click_rate_test.json', { + click_count: click_state.count, + result + }) + _.up('action_submitted', {}, { value: result, index: click_state.step_index }) + _.up('action_complete', {}, { value: result }) + return result + } + + function reset_clicks () { + click_state.count = 0 + click_state.start = 0 + click_state.complete = false + result_el.textContent = '' + update_count() + } + + function update_count () { count_el.textContent = click_state.count } + function show_result (result) { result_el.textContent = result } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { 0: '' }, + net_helper: { 0: '' } + }, + drive: { + 'style/': { + 'form_click_rate_test.css': { $ref: 'form_click_rate_test.css' } + }, + 'data/': { + 'form_click_rate_test.json': { raw: { click_count: 0, result: '' } } + }, + 'docs/': { 'README.md': { raw: '# Click Rate Test\nClick 10 times as fast as possible.' } } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/form_click_rate_test/form_click_rate_test.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],13:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_input +async function form_input (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + let input_accessible = true + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+ +
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const input_field_el = shadow.querySelector('.input-field') + const overlay_el = shadow.querySelector('.overlay-lock') + + input_field_el.oninput = on_input_field_input + + async function on_input_field_input () { + if (!input_accessible) return + await drive.put('data/form_input.json', { + input_field: input_field_el.value + }) + if (input_field_el.value.length >= 10) { + _.up('action_submitted', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + console.log('mark_as_complete') + } else { + _.up('action_incomplete', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + } + } + + await sdb.watch(onbatch) + const parent_handler = { + step_data, + reset_data + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + + function ondata (data) { + if (data.length > 0) { + const input_data = data[0] + if (input_data.input_field) { + input_field_el.value = input_data.input_field + } + } else { + input_field_el.value = '' + } + } + + function io_up () { + return function onmessage ({ type, data }) { + console.log('message from form_input', type, data) + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + function step_data (data, type) { + current_step = data + input_field_el.value = data?.data + + input_accessible = data.is_accessible !== false + + overlay_el.hidden = input_accessible + + input_field_el.placeholder = input_accessible + ? 'Type to submit' + : 'Input disabled for this step' + } + + function reset_data (data, type) { + input_field_el.value = '' + drive.put('data/form_input.json', { + input_field: '' + }) + } +} +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .input-display { + background: #131315; + border-radius: 16px; + border: 1px solid #3c3c3c; + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + position: relative; + } + .input-display:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .overlay-lock { + position: absolute; + inset: 0; + background: transparent; + z-index: 10; + cursor: not-allowed; + }` + } + }, + 'data/': { + 'form_input.json': { + raw: { + input_field: '' + } + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/form_input/form_input.js") +},{"STATE":1,"net_helper":20}],14:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_tile_split_choice +async function form_tile_split_choice (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
Split Tile
+
+ + + + +
+
Choose direction to split the tile
+
+ + ` + const style = shadow.querySelector('style') + const buttons = Array.from(shadow.querySelectorAll('.choice-btn')) + + buttons.forEach(btn => btn.addEventListener('click', on_choice_click)) + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { + style.replaceChildren(create_style_element()) + + function create_style_element () { + const style_el = document.createElement('style') + style_el.textContent = data[0] + return style_el + } + } + + function ondata (data) { + // support persisted/default choice if present + if (data.length > 0 && data[0] && data[0].choice) { + highlight_choice(String(data[0].choice)) + } + } + + function step_data (data) { + current_step = data + } + + function reset_data () { + // nothing for now + } + + async function on_choice_click (ev) { + const choice = ev.currentTarget.getAttribute('data-choice') + await drive.put('data/form_tile_split_choice.json', { choice }) + highlight_choice(choice) + _.up('action_submitted', {}, { value: choice, index: current_step?.index ?? 0 }) + + // If this is a single-step action, auto-complete the action + if (current_step && current_step.total_steps === 1) { + _.up('action_complete', {}, { value: choice }) + } + } + + function highlight_choice (choice) { + buttons.forEach(b => { + const isActive = b.getAttribute('data-choice') === choice + b.classList.toggle('active', isActive) + b.setAttribute('aria-pressed', isActive ? 'true' : 'false') + if (isActive) { + b.style.background = 'linear-gradient(180deg, rgba(103,195,255,0.06), rgba(103,195,255,0.02))' + b.style.boxShadow = '0 12px 36px rgba(103,195,255,0.16)' + b.style.borderColor = 'rgba(103,195,255,0.36)' + b.style.transform = 'translateY(-2px) scale(1.01)' + } else { + b.style.background = '' + b.style.boxShadow = '' + b.style.borderColor = '' + b.style.transform = '' + } + }) + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { $: '' } + // DOCS: { $: '' }, + } + } + + function fallback_instance () { + return { + _: { + net_helper: { 0: '' } + // DOCS: { 0: '' }, + }, + drive: { + 'style/': { + 'form_tile_split_choice.css': { $ref: 'form_tile_split_choice.css' } + }, + 'data/': { + 'form_tile_split_choice.json': { raw: { choice: null } } + }, + 'docs/': { 'README.md': { $ref: 'README.md' } } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/form_tile_split_choice/form_tile_split_choice.js") +},{"STATE":1,"net_helper":20}],15:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') +const graph_explorer = require('graph-explorer') +const graphdb = require('./graphdb') + +module.exports = graph_viewer + +async function graph_viewer (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + let db = null + let latest_entries = null + let graph_explorer_connected = false + + // Protocol + const { io, _ } = net(id) + io.on = { + up: io_up(), + graph_explorer: io_graph_explorer() + } + if (invite) io.accept(invite) + + const on = { + theme: inject, + entries: on_entries + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const subs = await sdb.watch(onbatch) + + const explorer_el = await graph_explorer(subs[0], io.invite('graph_explorer', { storage: id })) + graph_explorer_connected = true + if (latest_entries) _.graph_explorer('db_initialized', {}, { entries: latest_entries }) + shadow.append(explorer_el) + + return el + + function io_up () { + const parent_handlers = { + execute_step: parent_execute_step, + set_mode: parent_forward_to_graph_explorer, + set_search_query: parent_forward_to_graph_explorer, + select_nodes: parent_forward_to_graph_explorer, + expand_node: parent_forward_to_graph_explorer, + collapse_node: parent_forward_to_graph_explorer, + toggle_node: parent_forward_to_graph_explorer, + get_selected: parent_forward_to_graph_explorer, + get_confirmed: parent_forward_to_graph_explorer, + clear_selection: parent_forward_to_graph_explorer, + set_flag: parent_forward_to_graph_explorer, + scroll_to_node: parent_forward_to_graph_explorer, + docs_toggle: parent_forward_to_graph_explorer + } + return function onmessage (msg) { + const handler = parent_handlers[msg.type] || fail + handler(msg) + } + } + + function parent_execute_step (msg) { + const commands = get_step_commands(msg.data) + for (const command of commands) { + const refs = msg.head ? { cause: msg.head } : {} + const data = command.data !== undefined ? command.data : {} + _.graph_explorer(command.type, refs, data) + } + } + + function parent_forward_to_graph_explorer (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.graph_explorer(msg.type, refs, msg.data) + } + + function get_step_commands (data) { + if (!data) return [] + if (Array.isArray(data.commands)) return data.commands.filter(has_command_type) + if (data.command && has_command_type(data.command)) return [data.command] + if (has_command_type(data)) { + return [{ type: data.type, data: data.data !== undefined ? data.data : {} }] + } + return [] + + function has_command_type (command) { + return command && typeof command.type === 'string' && command.type.length > 0 + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const handler = on[type] || fail + handler({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + function inject ({ data }) { sheet.replaceSync(data.join('\n')) } + + function on_entries ({ data }) { + if (!data || !data[0]) { + console.error('Entries data is missing or empty.') + latest_entries = {} + db = graphdb({}) + if (graph_explorer_connected) _.graph_explorer('db_initialized', {}, { entries: {} }) + return + } + + let parsed_data + try { + parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + } catch (e) { + console.error('Failed to parse entries data:', e) + parsed_data = {} + } + + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + parsed_data = {} + } + + db = graphdb(parsed_data) + latest_entries = parsed_data + if (graph_explorer_connected) _.graph_explorer('db_initialized', {}, { entries: parsed_data }) + } + + // --------------------------------------------------------- + // PROTOCOL + // --------------------------------------------------------- + + function io_graph_explorer () { + return function graph_explorer_protocol(msg) { + const { type } = msg + const db_handler = { + db_get: params => db.get(params.path), + db_has: params => db.has(params.path), + db_is_empty: () => db.is_empty(), + db_root: () => db.root(), + db_keys: () => db.keys(), + db_raw: () => db.raw() + } + + if (type.startsWith('db_')) { + handle_db_request(msg) + } else { + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function handle_db_request ({ head, type, data }) { + const handler = db ? db_handler[type] || db_fail : db_fail + _.graph_explorer('db_response', { cause: head }, { result: handler(data) }) + + function db_fail () { + const msg = db ? '[graph_viewer] Unknown db operation:' : '[graph_viewer] Database not initialized yet' + console.warn(msg, type) + return null + } + } + } + } +} + +function fallback_module () { + return { + _: { + 'graph-explorer': { + $: '' + }, + './graphdb': { + $: '' + }, + net_helper: { + $: '' + } + }, + api: fallback_instance + } + + function fallback_instance () { + return { + _: { + 'graph-explorer': { + $: '', + 0: '', + mapping: { + style: 'theme', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + }, + './graphdb': { + $: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'theme/': { + 'style.css': { + raw: ` + :host { + display: block; + height: 100%; + width: 100%; + } + ` + } + }, + 'entries/': { + 'entries.json': { + $ref: 'entries.json' + } + }, + 'runtime/': { + 'node_height.json': { raw: '16' }, + 'vertical_scroll_value.json': { raw: '0' }, + 'horizontal_scroll_value.json': { raw: '0' }, + 'selected_instance_paths.json': { raw: '[]' }, + 'confirmed_selected.json': { raw: '[]' }, + 'instance_states.json': { raw: '{}' }, + 'search_entry_states.json': { raw: '{}' }, + 'last_clicked_node.json': { raw: 'null' }, + 'view_order_tracking.json': { raw: '{}' } + }, + 'mode/': { + 'current_mode.json': { raw: '"menubar"' }, + 'previous_mode.json': { raw: '"menubar"' }, + 'search_query.json': { raw: '""' }, + 'multi_select_enabled.json': { raw: 'false' }, + 'select_between_enabled.json': { raw: 'false' } + }, + 'flags/': { + 'hubs.json': { raw: '"default"' }, + 'selection.json': { raw: 'true' }, + 'recursive_collapse.json': { raw: 'true' } + }, + 'keybinds/': { + 'navigation.json': { + raw: JSON.stringify({ + ArrowUp: 'navigate_up_current_node', + ArrowDown: 'navigate_down_current_node', + 'Control+ArrowDown': 'toggle_subs_for_current_node', + 'Control+ArrowUp': 'toggle_hubs_for_current_node', + 'Alt+s': 'multiselect_current_node', + 'Alt+b': 'select_between_current_node', + 'Control+m': 'toggle_search_mode', + 'Alt+j': 'jump_to_next_duplicate' + }) + } + }, + 'undo/': { + 'stack.json': { raw: '[]' } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/graph_viewer/graph_viewer.js") +},{"./graphdb":16,"STATE":1,"graph-explorer":2,"net_helper":20}],16:[function(require,module,exports){ +module.exports = graphdb + +function graphdb (entries) { + // Validate entries + if (!entries || typeof entries !== 'object') { + console.warn('[graphdb] Invalid entries provided, using empty object') + entries = {} + } + + const api = { + get, + has, + keys, + is_empty, + root, + raw + } + + return api + + function get (path) { return entries[path] || null } + function has (path) { return path in entries } + function keys () { return Object.keys(entries) } + function is_empty () { return Object.keys(entries).length === 0 } + function root () { return entries['/'] || null } + function raw () { return entries } +} + +},{}],17:[function(require,module,exports){ +module.exports = { resource } + +function resource (timeout = 1000) { + const states = {} + return { set, get } + function load (pid) { return states[pid] || (states[pid] = { item: null, pending: [] }) } + function set (pid, item) { + const state = load(pid) + state.item = item + const { pending } = state + state.pending = [] + pending.map(resolve_pending_waiter) + + function resolve_pending_waiter (waiter) { waiter.resolve(item) } + } + function get (pid) { + return new Promise(on) + function on (resolve, reject) { + const { item, pending } = load(pid) + if (item) return resolve(item) + pending.push({ resolve, reject }) + } + } +} + +},{}],18:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = input_test +async function input_test (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + let input_accessible = true + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
Testing 2nd Type
+
+ + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const input_field_el = shadow.querySelector('.input-field') + const overlay_el = shadow.querySelector('.overlay-lock') + + input_field_el.oninput = on_input_field_input + + async function on_input_field_input () { + if (!input_accessible) return + + await drive.put('data/input_test.json', { + input_field: input_field_el.value + }) + + if (input_field_el.value.length >= 10) { + _.up('action_submitted', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + console.log('mark_as_complete') + } else { + _.up('action_incomplete', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + } + } + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + + function ondata (data) { + if (data.length > 0) { + const input_data = data[0] + if (input_data.input_field) { + input_field_el.value = input_data.input_field + } + } else { + input_field_el.value = '' + } + } + + // ------------------ + // Parent Observer + // ------------------ + + function io_up () { + return function onmessage ({ type, data }) { + console.log('message from input_test', type, data) + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + function step_data (data, type) { + current_step = data + + input_accessible = data.is_accessible !== false + + overlay_el.hidden = input_accessible + + input_field_el.placeholder = input_accessible + ? 'Type to submit' + : 'Input disabled for this step' + } + + function reset_data (data, type) { + input_field_el.value = '' + drive.put('data/input_test.json', { + input_field: '' + }) + } +} +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .title { + color: #e8eaed; + font-size: 18px; + display: flex; + align-items: center; + } + .input-display { + position: relative; + background: #131315; + border-radius: 16px; + border: 1px solid #3c3c3c; + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + } + .input-display:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .overlay-lock { + position: absolute; + inset: 0; + background: transparent; + z-index: 10; + cursor: not-allowed; + }` + } + }, + 'data/': { + 'input_test.json': { + raw: { + input_field: '' + } + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/input_test/input_test.js") +},{"STATE":1,"net_helper":20}],19:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) + +module.exports = create_component_menu +async function create_component_menu (opts, names, inicheck, callbacks) { + const { sdb } = await get(opts.sid) + const { drive } = sdb + const on = { + style: inject + } + const { + on_checkbox_change, + on_label_click, + on_select_all_toggle, + on_resize_toggle + } = callbacks + + const checkobject = {} + inicheck.forEach(mark_checked_index) + + function mark_checked_index (checked_position) { checkobject[checked_position - 1] = true } + + const all_checked = inicheck.length === 0 || Object.keys(checkobject).length === names.length + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` + ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const menu = shadow.querySelector('.menu') + const toggle_btn = shadow.querySelector('.menu-toggle-button') + const unselect_btn = shadow.querySelector('.unselect-all-button') + const resize_btn = shadow.querySelector('.resize-toggle-button') + const list = shadow.querySelector('.menu-list') + + names.forEach(create_menu_item) + + function create_menu_item (name, index) { + const is_checked = all_checked || checkobject[index] === true + const menu_item = document.createElement('li') + menu_item.className = 'menu-item' + menu_item.innerHTML = ` + ${name} + + ` + list.appendChild(menu_item) + + const checkbox = menu_item.querySelector('input') + const label = menu_item.querySelector('span') + + checkbox.onchange = on_checkbox_change_event + label.onclick = on_label_click_event + + function on_checkbox_change_event (e) { on_checkbox_change({ index, checked: e.target.checked }) } + function on_label_click_event () { + on_label_click({ index, name }) + menu.classList.add('hidden') + } + } + await sdb.watch(onbatch) + // event listeners + console.log('resize_btn', resize_btn) + toggle_btn.onclick = on_toggle_btn + unselect_btn.onclick = on_unselect_btn + resize_btn.onclick = on_resize_btn + document.onclick = handle_document_click + + return el + + function on_toggle_btn (e) { + e.stopPropagation() + menu.classList.toggle('hidden') + } + + function on_unselect_btn () { + const select_all = unselect_btn.textContent === 'Select All' + unselect_btn.textContent = select_all ? 'Unselect All' : 'Select All' + list.querySelectorAll('input[type="checkbox"]').forEach(update_checkbox_state) + on_select_all_toggle({ selectAll: select_all }) + + function update_checkbox_state (checkbox) { checkbox.checked = select_all } + } + + function on_resize_btn () { + console.log('on_resize_btn') + on_resize_toggle() + } + + function handle_document_click (e) { + const path = e.composedPath() + if (!menu.classList.contains('hidden') && !path.includes(el)) { + menu.classList.add('hidden') + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} +function fallback_module () { + return { + api: fallback_instance + } + function fallback_instance () { + return { + drive: { + 'style/': { + 'theme.css': { + raw: ` + :host { + display: block; + position: sticky; + top: 0; + z-index: 100; + background-color: #e0e0e0; + } + + .nav-bar-container-inner { + } + + .nav-bar { + display: flex; + position: relative; + justify-content: center; + align-items: center; + padding: 10px 20px; + border-bottom: 2px solid #333; + min-height: 30px; + } + + .menu-toggle-button { + padding: 10px; + background-color: #e0e0e0; + border: none; + cursor: pointer; + border-radius: 5px; + font-weight: bold; + } + + .menu-toggle-button:hover { + background-color: #d0d0d0; + } + + .menu.hidden { + display: none; + } + + .menu { + display: block; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + width: 250px; + max-width: 90%; + background-color: #f0f0f0; + padding: 10px; + border-radius: 0 0 5px 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + z-index: 101; + } + + .menu-header { + margin-bottom: 10px; + text-align: center; + } + + .unselect-all-button { + padding: 8px 12px; + border: none; + background-color: #d0d0d0; + cursor: pointer; + border-radius: 5px; + width: 100%; + margin-bottom: 5px; + } + + .unselect-all-button:hover { + background-color: #c0c0c0; + } + + .resize-toggle-button { + padding: 8px 12px; + border: none; + background-color: #d0d0d0; + cursor: pointer; + border-radius: 5px; + width: 100%; + } + + .resize-toggle-button:hover { + background-color: #c0c0c0; + } + + .menu-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 400px; + overflow-y: auto; + background-color: #f0f0f0; + } + + .menu-list::-webkit-scrollbar { + width: 8px; + } + + .menu-list::-webkit-scrollbar-track { + background: #f0f0f0; + } + + .menu-list::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 4px; + } + + .menu-list::-webkit-scrollbar-thumb:hover { + background: #bbb; + } + + .menu-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 5px; + border-bottom: 1px solid #ccc; + } + + .menu-item span { + cursor: pointer; + flex-grow: 1; + margin-right: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .menu-item span:hover { + color: #007bff; + } + + .menu-item:last-child { + border-bottom: none; + } + + .menu-item input[type="checkbox"] { + flex-shrink: 0; + }` + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/menu/menu.js") +},{"STATE":1}],20:[function(require,module,exports){ +(function (__filename){(function (){ +module.exports = net + +function net (id) { + const [label, io, _, sub, hub] = [`[${id}@${__filename}]`, { invite, accept, on: {} }, {}, {}, {}] + return { io, _ } + function forward (to, M) { + if (to.startsWith(id)) { + const ups = [...new Set(Object.keys(hub).map(id => hub[id].tx))] + for (const tx of ups) tx(M) + return + } + for (const id of Object.keys(sub)) if (to.startsWith(id)) return sub[id].tx(M) + throw new Error(`${label} unknown recipient "${to}"`) + } + function invite (name, ids) { + if (!io.on[name]) throw new Error(`${label} no protocol handler for "${name}"`) + return Object.assign(invite, { ids }) + function invite (tx) { + const rx = router(sub) + add(name, tx, tx.id, rx, sub) + return rx + } + } + function accept (invite) { + const rx = router(hub) + const tx = invite(Object.assign(rx, { id })) + for (const [name, to] of Object.entries(invite.ids)) { + if (hub[to]) throw new Error(`${label} already connected to "${to}"`) + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + add(name, tx, to, rx, hub) + } + } + function router ($) { + return function rx (M) { + const { head: [by, to] } = M + console.log(`[M]\n${by} \n to: \n ${to}`, M) + if (to !== id) return forward(to, M) + if (!$[by]) throw new Error(`${label} unknown sender "${by}"`) + const { name } = $[by].state + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + io.on[name](M) + } + } + function add (name, tx, to, rx, $) { + const state = { name, to, mid: 0 } + _[name] = send + $[to] = { rx, tx, state } + function send (type, refs = {}, data = null) { + const head = [id, to, state.mid++] + const meta = { time: Date.now(), stack: (new Error().stack) } + tx({ head, refs, type, data, meta }) + return head + } + } +} + +}).call(this)}).call(this,"/src/node_modules/net_helper/net_helper.js") +},{}],21:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const form_input = require('form_input') +const input_test = require('input_test') +const form_tile_split_choice = require('form_tile_split_choice') +const form_click_rate_test = require('form_click_rate_test') + +program.form_input = form_input +program.input_test = input_test +program.form_tile_split_choice = form_tile_split_choice +program.form_click_rate_test = form_click_rate_test + +module.exports = program + +async function program (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + variables: onvariables + } + + const { io } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + await sdb.watch(onbatch) + + const parent_handler = { + display_result, + update_data + } + + return el + + // --- Internal Functions --- + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + + function onvariables (data) { + // Dont get why we have this module. + } + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + function display_result (data) { + console.log('Display Result:', data) + alert(`Result of action(${data.selected_action ? data.selected_action : 'unknown'}): ${data.result ? data.result : 'no result'}`) + } + function update_data (data) { drive.put('variables/program.json', data) } +} + +// --- Fallback Module --- +function fallback_module () { + return { + api: fallback_instance, + _: { + form_input: { $: '' }, + input_test: { $: '' }, + form_tile_split_choice: { $: '' }, + form_click_rate_test: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + net_helper: { 0: '' } + }, + drive: { + 'style/': { + 'program.css': { + raw: ` + .main { + display: flex; + flex-direction: column; + align-items: center; + } + ` + } + }, + 'variables/': { + 'program.json': { $ref: 'program.json' } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/program/program.js") +},{"STATE":1,"form_click_rate_test":12,"form_input":13,"form_tile_split_choice":14,"input_test":18,"net_helper":20}],22:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +// const docs = DOCS(__filename)() +const net = require('net_helper') + +const console_history = require('console_history') +const actions = require('actions') +const tabbed_editor = require('tabbed_editor') +const graph_viewer = require('graph_viewer') +const docs_window = require('docs_window') + +module.exports = program_container + +async function program_container (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ + + + + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const program_main = shadow.querySelector('.program-container') + const graph_explorer_placeholder = shadow.querySelector('graph-explorer-placeholder') + const actions_placeholder = shadow.querySelector('actions-placeholder') + const tabbed_editor_placeholder = shadow.querySelector('tabbed-editor-placeholder') + const console_placeholder = shadow.querySelector('console-history-placeholder') + const docs_window_placeholder = shadow.querySelector('docs-window-placeholder') + + let console_history_el = null + let docs_window_el = null + let actions_el = null + let tabbed_editor_el = null + let graph_explorer_el = null + + const subs = await sdb.watch(onbatch) + + io.on = { + up: io_up(), + console_history: io_console_history(), + actions: io_actions(), + tabbed_editor: io_tabbed_editor(), + graph_explorer: io_graph_explorer(), + docs_window: io_docs_window() + } + if (invite) io.accept(invite) + + actions_el = await actions({ ...subs[1] }, io.invite('actions', { up: id })) + actions_el.classList.add('actions') + actions_placeholder.replaceWith(actions_el) + + tabbed_editor_el = await tabbed_editor({ ...subs[2] }, io.invite('tabbed_editor', { up: id })) + tabbed_editor_el.classList.add('tabbed-editor') + tabbed_editor_placeholder.replaceWith(tabbed_editor_el) + + docs_window_el = await docs_window({ ...subs[4] }, io.invite('docs_window', { up: id })) + docs_window_el.classList.add('docs-window') + docs_window_el.classList.add('hide') + docs_window_placeholder.replaceWith(docs_window_el) + + graph_explorer_el = await graph_viewer({ ...subs[3] }, io.invite('graph_explorer', { up: id })) + graph_explorer_el.classList.add('graph-explorer') + graph_explorer_placeholder.replaceWith(graph_explorer_el) + + console_history_el = await console_history({ ...subs[0] }, io.invite('console_history', { up: id })) + console_history_el.classList.add('console-history') + console_placeholder.replaceWith(console_history_el) + let console_view = false + let actions_view = false + let graph_explorer_view = false + + if (invite) { + console_history_el.classList.add('hide') + actions_el.classList.add('hide') + tabbed_editor_el.classList.add('show') + graph_explorer_el.classList.add('hide') + + // Send message to root to set doc display handler + _.up('set_doc_display_handler', {}, { callback: on_doc_display }) + } + + if (!invite) { + actions_view = !actions_el.classList.contains('hide') + console_view = !console_history_el.classList.contains('hide') + graph_explorer_view = !graph_explorer_el.classList.contains('hide') + } + update_program_layout() + + return el + + function console_history_toggle_view () { + const next_view = !console_view + set_panel_visibility(console_history_el, next_view) + console_view = next_view + update_program_layout() + } + + function actions_toggle_view (display_data) { + const next_view = resolve_display_state(display_data, actions_view) + set_panel_visibility(actions_el, next_view) + actions_view = next_view + update_program_layout() + } + + function graph_explorer_toggle_view () { + const next_view = !graph_explorer_view + set_panel_visibility(graph_explorer_el, next_view) + graph_explorer_view = next_view + update_program_layout() + } + + function resolve_display_state (display_data, current_view) { + if (typeof display_data === 'boolean') return display_data + if (typeof display_data === 'string') return display_data !== 'none' + if (typeof display_data === 'object' && display_data.display !== undefined) return display_data.display !== 'none' + return !current_view + } + + function set_panel_visibility (panel_el, visible) { + if (visible) { + panel_el.classList.remove('hide') + panel_el.classList.add('show') + } else { + panel_el.classList.remove('show') + panel_el.classList.add('hide') + } + } + + function tabbed_editor_toggle_view (show = true) { + if (show) { + set_panel_visibility(tabbed_editor_el, true) + set_panel_visibility(actions_el, false) + set_panel_visibility(console_history_el, false) + set_panel_visibility(graph_explorer_el, false) + actions_view = false + console_view = false + graph_explorer_view = false + } else { + set_panel_visibility(tabbed_editor_el, false) + } + update_program_layout() + } + + function update_program_layout () { + const tabbed_visible = !tabbed_editor_el.classList.contains('hide') + const graph_visible = !graph_explorer_el.classList.contains('hide') + const actions_visible = !actions_el.classList.contains('hide') + const console_visible = !console_history_el.classList.contains('hide') + const has_primary = tabbed_visible || graph_visible + + let tabbed_row = '0px' + let graph_row = '0px' + let actions_row = '0px' + let console_row = '0px' + + if (tabbed_visible) { + tabbed_row = graph_visible ? 'minmax(120px, 1fr)' : 'minmax(80px, 1fr)' + } + if (graph_visible) { + graph_row = tabbed_visible ? 'minmax(150px, 1fr)' : 'minmax(200px, 1fr)' + } + if (actions_visible) { + if (!has_primary && !console_visible) actions_row = 'minmax(80px, 1fr)' + else actions_row = 'fit-content(260px)' + } + if (console_visible) { + if (!has_primary && !actions_visible) console_row = 'minmax(80px, 1fr)' + else console_row = 'fit-content(260px)' + } + if (!tabbed_visible && !graph_visible && !actions_visible && !console_visible) { + tabbed_row = '1fr' + } + + program_main.style.gridTemplateRows = `${tabbed_row} ${graph_row} ${actions_row} ${console_row}` + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + function inject ({ data }) { sheet.replaceSync(data[0]) } + + function on_doc_display (display_data) { + const { content, sid } = display_data + docs_window_el.classList.remove('hide') + _.docs_window('display_doc', {}, { content, sid }) + } + + // --------- + // PROTOCOLS + // --------- + + function io_console_history () { + return function console_history_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_actions () { + return function actions_protocol (msg) { + const action_handlers = { + selected_action: actions_selected_action, + ui_focus_docs: actions_ui_focus_docs, + ui_focus: actions_forward_up + } + + const handler = action_handlers[msg.type] || actions_forward_up + handler(msg) + + function actions_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function actions_selected_action (msg) { + const { data } = msg + _.up('update_quick_actions_input', msg.head ? { cause: msg.head } : {}, data) + } + + function actions_ui_focus_docs (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function io_tabbed_editor () { + return function tabbed_editor_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_graph_explorer () { + return function graph_explorer_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_docs_window () { + return function docs_window_protocol (msg) { + const action_handlers = { + close_docs: docs_window_close_docs + } + const handler = action_handlers[msg.type] || docs_window_noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function docs_window_close_docs () { docs_window_el.classList.add('hide') } + function docs_window_noop () {} + } + } + + function io_up () { + return function onmessage (msg) { + const action_handlers = { + console_history_toggle: onmessage_console_history_toggle, + graph_explorer_toggle: onmessage_graph_explorer_toggle, + display_actions: onmessage_display_actions, + filter_actions: onmessage_filter_actions, + tab_name_clicked: onmessage_tab_name_clicked, + tab_close_clicked: onmessage_tab_close_clicked, + switch_tab: onmessage_switch_tab, + entry_toggled: onmessage_entry_toggled, + execute_step: onmessage_execute_step, + display_doc: onmessage_display_doc, + load_actions: onmessage_send_actions, + update_actions_for_app: onmessage_send_actions + } + const handler = action_handlers[msg.type] || fail + handler(msg) + + function onmessage_console_history_toggle () { console_history_toggle_view() } + function onmessage_graph_explorer_toggle () { graph_explorer_toggle_view() } + function onmessage_display_actions (msg) { actions_toggle_view(msg.data) } + function onmessage_filter_actions (msg) { _.actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_tab_close_clicked (msg) { + _.tabbed_editor('close_tab', msg.head ? { cause: msg.head } : {}, msg.data) + _.console_history('record_closed_tab', msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_entry_toggled (msg) { _.graph_explorer(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_execute_step (msg) { + if (!msg.data || !Array.isArray(msg.data.commands) || msg.data.commands.length === 0) return + set_panel_visibility(graph_explorer_el, true) + graph_explorer_view = true + update_program_layout() + _.graph_explorer(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_send_actions (msg) { _.actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_tab_name_clicked (msg) { + tabbed_editor_toggle_view(true) + _.tabbed_editor('toggle_tab', msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_switch_tab (msg) { + tabbed_editor_toggle_view(true) + _.tabbed_editor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_display_doc (msg) { + docs_window_el.classList.remove('hide') + _.docs_window(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + console_history: { + $: '' + }, + actions: { + $: '' + }, + tabbed_editor: { + $: '' + }, + graph_viewer: { + $: '' + }, + docs_window: { + $: '' + }, + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + + function fallback_instance () { + return { + _: { + console_history: { + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + docs: 'docs', + actions: 'actions' + } + }, + actions: { + 0: '', + mapping: { + style: 'style', + actions: 'actions', + icons: 'icons', + hardcons: 'hardcons', + docs: 'docs' + } + }, + tabbed_editor: { + 0: '', + mapping: { + style: 'style', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + docs: 'docs' + } + }, + graph_viewer: { + 0: '', + mapping: { + theme: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + }, + docs_window: { + 0: '', + mapping: { + style: 'docs_style' + } + }, + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .program-container { + display: grid; + grid-template-columns: minmax(0, 1fr); + min-height: 200px; + height: 100%; + background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); + position: relative; + gap: 0; + padding: 0; + overflow: hidden; + container-type: size; + } + .docs-window { + position: absolute; + inset: 12px; + z-index: 20; + } + .tabbed-editor { + grid-row: 1; + grid-column: 1; + min-height: 0; + min-width: 0; + width: 100%; + height: 100%; + } + .graph-explorer { + grid-row: 2; + grid-column: 1; + min-height: 0; + min-width: 0; + width: 100%; + height: 100%; + } + .console-history { + grid-row: 4; + grid-column: 1; + display: flex; + flex-direction: column; + position: relative; + width: 100%; + height: 100%; + max-height: min(400px, 100%); + min-height: 0; + min-width: 0; + background-color: #161b22; + border: 1px solid #21262d; + border-radius: 6px; + box-sizing: border-box; + overflow: hidden; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + } + .actions { + grid-row: 3; + grid-column: 1; + position: relative; + background-color: #161b22; + border: 1px solid #21262d; + border-radius: 6px; + overflow: hidden; + } + .tabbed-editor { + position: relative; + width: 100%; + min-width: 0; + background-color: #0d1117; + border: 1px solid #21262d; + border-radius: 6px; + overflow: hidden; + } + .show { + display: block; + } + .hide { + display: none; + } + ` + } + }, + 'entries/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'commands/': {}, + 'icons/': {}, + 'scroll/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'active_tab/': {}, + 'runtime/': {}, + 'mode/': {}, + 'undo/': {}, + 'docs_style/': {} + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/program_container/program_container.js") +},{"STATE":1,"actions":9,"console_history":10,"docs_window":11,"graph_viewer":15,"net_helper":20,"tabbed_editor":27}],23:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = quick_actions + +async function quick_actions (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: iconject, + hardcons: onhardcons, + actions: onactions, + prefs: onprefs + } + + const el = document.createElement('div') + el.style.display = 'flex' + el.style.flex = 'auto' + + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+ +
+
` + const container = shadow.querySelector('.quick-actions-container') + const default_actions = shadow.querySelector('.default-actions') + const text_bar = shadow.querySelector('.text-bar') + const input_wrapper = shadow.querySelector('.input-wrapper') + const slash_prefix = shadow.querySelector('.slash-prefix') + const command_text = shadow.querySelector('.command-text') + const input_field = shadow.querySelector('.input-field') + const confirm_btn = shadow.querySelector('.confirm-btn') + const submit_btn = shadow.querySelector('.submit-btn') + const close_btn = shadow.querySelector('.close-btn') + const step_display = shadow.querySelector('.step-display') + const current_step = shadow.querySelector('.current-step') + const total_steps = shadow.querySelector('.total-step') + const tooltip = shadow.querySelector('.tooltip') + const input_tooltip = shadow.querySelector('.input-tooltip') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + let init = false + let enable_quick_action_tooltips = false + let enable_input_field_tooltips = false + let icons = {} + let hardcons = {} + let defaults = [] + let stored_selected_action = '' + let action_selected = false + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + const ui_actions = [ + create_action('Open Quick Actions', 'Open the quick action input.', activate_input_field), + create_action('Close Quick Actions', 'Close the quick action input.', deactivate_input_field), + create_action('Confirm Quick Action', 'Continue with the selected action.', confirm_action), + create_action('Submit Quick Action', 'Submit the selected action.', submit_action) + ] + register_actions() + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + on_open.info = ui_actions[0].info + on_close.info = ui_actions[1].info + on_confirm.info = ui_actions[2].info + on_submit.info = ui_actions[3].info + text_bar.onclick = docs.wrap_isolated(on_open) + close_btn.onclick = docs.wrap_isolated(on_close) + confirm_btn.onclick = docs.wrap_isolated(on_confirm) + submit_btn.onclick = docs.wrap_isolated(on_submit) + input_field.oninput = oninput + + await sdb.watch(onbatch) + + return el + + function oninput (e) { + const value = e.target.value + if (enable_input_field_tooltips) update_input_tooltip(value) + _.up('filter_actions', {}, value) + } + + function update_input_display (selected_action = null) { + if (selected_action) { + action_selected = true + slash_prefix.style.display = 'inline' + command_text.style.display = 'inline' + command_text.textContent = `#${selected_action.name}` + current_step.textContent = selected_action.current_step ? selected_action.current_step : 1 + total_steps.textContent = selected_action.total_steps ? selected_action.total_steps : 1 + step_display.style.display = 'inline-flex' + + input_field.style.display = 'none' + confirm_btn.style.display = 'flex' + hide_input_tooltip() + } else { + slash_prefix.style.display = 'none' + command_text.style.display = 'none' + input_field.style.display = 'block' + confirm_btn.style.display = 'none' + submit_btn.style.display = 'none' + step_display.style.display = 'none' + input_field.placeholder = 'Type to search actions...' + hide_input_tooltip() + action_selected = false + } + } + + function activate_input_field () { + if (action_selected) return + default_actions.style.display = 'none' + text_bar.style.display = 'none' + + input_wrapper.style.display = 'flex' + input_field.focus() + + if (enable_input_field_tooltips) update_input_tooltip('') + + _.up('display_actions', {}, { display: 'block', reason: 'browse' }) + } + + function io_up () { + return function onmessage (msg) { + const { type, data } = msg + // No need to handle docs_toggle - DOCS module handles it globally + const message_map = { + deactivate_input_field, + show_submit_btn, + update_current_step, + hide_submit_btn, + update_quick_actions_for_app, + update_input_command + } + const handler = message_map[type] || fail + handler(data) + } + } + + function deactivate_input_field (data = {}) { + const reason = data.reason ? data.reason : 'cancel' + + default_actions.style.display = 'flex' + text_bar.style.display = 'flex' + + input_wrapper.style.display = 'none' + + input_field.value = '' + update_input_display() + hide_input_tooltip() + + _.up('display_actions', {}, { display: 'none', reason }) + } + + function show_submit_btn () { + submit_btn.style.display = 'flex' + confirm_btn.style.display = 'none' + } + function hide_submit_btn () { submit_btn.style.display = 'none' } + + function update_current_step (data) { + const current_step_value = data.index !== undefined ? data.index + 1 : 1 + current_step.textContent = current_step_value + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + create_default_actions(defaults) + init = true + } else { + // TODO: update actions + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn(`Invalid message type: ${type}`, { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function onhardcons (data) { + hardcons = { + submit: data[0], + cross: data[1], + confirm: data[2] + } + submit_btn.innerHTML = hardcons.submit + close_btn.innerHTML = hardcons.cross + confirm_btn.innerHTML = hardcons.confirm + } + function iconject (data) { icons = data } + + function onactions (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + defaults = vars + register_actions() + create_default_actions(defaults) + } + + function onprefs (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + enable_input_field_tooltips = vars.input_field + enable_quick_action_tooltips = vars.quick_actions + } + + function create_default_actions (actions) { + default_actions.replaceChildren() + actions.forEach(create_action_button) + } + + function create_action_button (action) { + const btn = document.createElement('div') + btn.classList.add('action-btn') + if (icons[action.icon] === undefined) { + const texon = action.name.substring(0, 2) + btn.innerHTML = texon + } else { + btn.innerHTML = icons[action.icon] + } + if (enable_quick_action_tooltips) { + btn.onmouseenter = on_action_btn_mouseenter + btn.onmouseleave = hide_tooltip + } + on_action_click.info = action.info + on_action_click.opts = { state: { name: action.name } } + btn.onclick = docs.wrap_isolated(on_action_click) + default_actions.appendChild(btn) + + function on_action_click (event, $) { $($.state.name) } + function on_action_btn_mouseenter () { show_tooltip(btn, action.name) } + } + + function update_input_tooltip (value) { + if (!value || value.trim() === '') { + hide_input_tooltip() + return + } + const tooltip_text = get_tooltip_text(value) + if (tooltip_text) { + show_input_tooltip(tooltip_text) + } else { + hide_input_tooltip() + } + } + + function get_tooltip_text (value) { + const lower_value = value.toLowerCase().trim() + if (lower_value.length === 0) return null + if (defaults.length > 0) { + const matching = defaults.filter(matches_action_for_tooltip) + if (matching.length > 0) { + const names = matching.map(get_action_name) + return `Found ${matching.length} action${matching.length > 1 ? 's' : ''}: ${names.join(', ')}` + } + } + return 'No actions found. Try a different search term.' + + function matches_action_for_tooltip (action) { return matches_action(action, lower_value) } + function get_action_name (action) { return action.name } + } + + function matches_action (action, search_term) { return action.name.toLowerCase().includes(search_term) } + + function show_input_tooltip (text) { + input_tooltip.textContent = text + input_tooltip.style.display = 'block' + position_input_tooltip() + } + + function hide_input_tooltip () { input_tooltip.style.display = 'none' } + + function position_input_tooltip () { + const input_rect = input_field.getBoundingClientRect() + const wrapper_rect = input_wrapper.getBoundingClientRect() + const tooltip_rect = input_tooltip.getBoundingClientRect() + const left = input_rect.left - wrapper_rect.left + (input_rect.width / 2) - (tooltip_rect.width / 2) + const top = input_rect.top - wrapper_rect.top - tooltip_rect.height - 8 + input_tooltip.style.left = `${left}px` + input_tooltip.style.top = `${top}px` + } + + function update_quick_actions_for_app (data) { + if (data) { + drive.put('actions/default.json', data) + } + } + + function update_input_command (command) { + if (action_selected) return + stored_selected_action = command + if (input_wrapper.style.display === 'none') { + default_actions.style.display = 'none' + text_bar.style.display = 'none' + input_wrapper.style.display = 'flex' + input_field.focus() + if (enable_input_field_tooltips) update_input_tooltip('') + } + + // Find the action that matches the command + const matching_action = defaults.find(matches_selected_command) + const selected = matching_action || command + + if (matching_action) { + const pass_data = { + name: matching_action.name, + current_step: 1, + total_steps: matching_action.steps ? matching_action.steps.length : 1 + } + update_input_display(pass_data) + } else { + const pass_data = { + name: typeof command === 'string' ? command : command.name, + current_step: 1, + total_steps: 3 + } + update_input_display(pass_data) + } + + _.up('display_actions', {}, { display: 'none', reason: 'selected' }) + _.up('activate_steps_wizard', {}, stored_selected_action) + + function matches_selected_command (action) { + const target = typeof command === 'string' ? command : command?.name + return action.name === target + } + } + + function create_action (name, info, run) { + return { name, info, icon: 'action', status: { hidden: true }, steps: [], run } + } + + function register_actions () { + docs.register_actions(ui_actions.concat(defaults.map(bind_action))) + + function bind_action (action) { + return { ...action, run: select_action } + + function select_action () { _.up('update_quick_actions_input', {}, action) } + } + } + + function on_open (event, $) { $('Open Quick Actions') } + function on_close (event, $) { $('Close Quick Actions') } + function on_confirm (event, $) { $('Confirm Quick Action') } + function on_submit (event, $) { $('Submit Quick Action') } + function confirm_action () { _.up('activate_steps_wizard', {}, stored_selected_action) } + function submit_action () { _.up('action_submitted', {}, null) } + + function show_tooltip (btn, name) { + tooltip.textContent = name + tooltip.style.display = 'block' + const btn_rect = btn.getBoundingClientRect() + const container_rect = container.getBoundingClientRect() + const tooltip_rect = tooltip.getBoundingClientRect() + const left = btn_rect.left - container_rect.left + (btn_rect.width / 2) - (tooltip_rect.width / 2) + const top = btn_rect.top - container_rect.top - tooltip_rect.height - 8 + tooltip.style.left = `${left}px` + tooltip.style.top = `${top}px` + } + + function hide_tooltip () { tooltip.style.display = 'none' } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + '0.svg': { + $ref: 'action1.svg' + }, + '1.svg': { + $ref: 'action2.svg' + }, + '2.svg': { + $ref: 'action1.svg' + }, + '3.svg': { + $ref: 'action2.svg' + }, + '4.svg': { + $ref: 'action1.svg' + } + }, + 'hardcons/': { + 'submit.svg': { + $ref: 'submit.svg' + }, + 'close.svg': { + $ref: 'cross.svg' + }, + 'confirm.svg': { + $ref: 'check.svg' + } + }, + 'actions/': { + 'default.json': { + raw: JSON.stringify([]) + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .quick-actions-container { + display: flex; + flex: auto; + flex-direction: row; + align-items: center; + background: #191919; + border-radius: 20px; + gap: 8px; + min-width: 200px; + position: relative; + } + .default-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 0 4px; + } + .action-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + } + .action-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + .text-bar { + flex: 1; + height: 24px; + margin: 4px; + border-radius: 16px; + background: #131315; + cursor: pointer; + user-select: none; + } + .text-bar:hover { + background: #1a1a1c; + } + .input-wrapper { + display: flex; + flex: 1; + align-items: center; + background: #131315; + border-radius: 16px; + width: auto; + height: 30px; + border: 1px solid #3c3c3c; + } + .input-wrapper:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-display { + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + position: relative; + } + .slash-prefix { + color: #a6a6a6; + font-size: 14px; + margin-right: 4px; + display: none; + } + .command-text { + color: #e8eaed; + font-size: 14px; + background: #2d2d2d; + border: 1px solid #4285f4; + border-radius: 4px; + padding: 2px 6px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + display: none; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .submit-btn { + display: none; + align-items: center; + justify-content: center; + background: #ffffff00; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: white; + min-width: 32px; + height: 32px; + margin-right: 4px; + font-size: 12px; + } + .submit-btn:hover { + background: #ffffff00; + } + .confirm-btn { + display: none; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + min-width: 32px; + height: 32px; + margin-right: 4px; + font-size: 12px; + } + .confirm-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + .close-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + min-width: 32px; + height: 32px; + } + .close-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + svg { + width: 16px; + height: 16px; + } + .step-display { + display: inline-flex; + align-items: center; + gap: 2px; + margin-left: 8px; + background: #2d2d2d; + border: 1px solid #666; + border-radius: 4px; + padding: 1px 6px; + font-size: 12px; + color: #fff; + font-family: monospace; + } + .current-step { + color:#f0f0f0; + } + .step-separator { + color: #888; + } + .total-step { + color: #f0f0f0; + } + .hide { + display: none; + } + .tooltip { + position: absolute; + background: #2d2d2d; + color: #e8eaed; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid #3c3c3c; + } + .tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #2d2d2d; + } + .input-tooltip { + position: absolute; + background: #2d2d2d; + color: #e8eaed; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: normal; + pointer-events: none; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid #4285f4; + max-width: 300px; + word-wrap: break-word; + } + .input-tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #4285f4; + } + ` + } + }, + 'prefs/': { + 'tooltips.json': { + raw: JSON.stringify({ + quick_actions: true, + input_field: false + }) + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/quick_actions/quick_actions.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],24:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const { resource } = require('helpers') + +module.exports = quick_editor +let is_called +const nesting = 0 + +async function quick_editor (opts) { + // ---------------------------------------- + let init; let data; let port; let labels; let nesting_limit; let top_first; let select = [] + const current_data = {} + + const { sdb, io, net } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + // ---------------------------------------- + const el = document.createElement('div') + el.classList.add('quick-editor') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` + +
+ +
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const menu_btn = shadow.querySelector('.dots-button') + const menu = shadow.querySelector('.quick-menu') + const import_btn = shadow.querySelector('.button.import') + const export_btn = shadow.querySelector('.button.export') + const input = shadow.querySelector('input') + const apply_btn = shadow.querySelector('.button') + // ---------------------------------------- + // EVENTS + // ---------------------------------------- + await sdb.watch(onbatch) + menu_btn.onclick = on_menu_btn_click + + function on_menu_btn_click () { menu_click(false) } + + if (is_called) { + apply_btn.onclick = apply + menu_btn.onclick = on_called_menu_btn_click + + function on_called_menu_btn_click () { menu_click(true) } + + labels = ['Nodes', 'Types', 'Files'] + nesting_limit = nesting + 3 + top_first = 0 + } else { + apply_btn.onclick = on_apply_switch_click + input.onchange = upload + import_btn.onclick = on_import_btn_click + export_btn.onclick = on_export_btn_click + + function on_apply_switch_click () { port.postMessage({ type: 'swtch', data: [{ name: current_data.Types.trim(), type: current_data.Names.trim() }] }) } + function on_import_btn_click () { input.click() } + function on_export_btn_click () { + if (current_data.radio.name === 'Names') { + port.postMessage({ type: 'export_db', data: [{ name: current_data.Names.trim(), type: current_data.Types.trim() }] }) + } else { + port.postMessage({ type: 'export_root', data: [{ name: current_data.Root.trim(), type: current_data.Nodes.trim() }] }) + } + } + + menu.classList.add('admin') + labels = ['Root', 'Types', 'Names', 'Nodes', 'Files', 'Entries'] + nesting_limit = nesting + 6 + top_first = 1 + select = [1, 0, 1, 0, 0, 0] + } + + // ---------------------------------------- + // IO + // ---------------------------------------- + const item = resource() + io.on(register_port_channel) + + function register_port_channel (port) { + const { by, to } = port + item.set(port.to, port) + + port.onmessage = on_port_message + + function on_port_message (event) { + const txt = event.data + const key = `[${by} -> ${to}]` + console.log(key) + data = txt + if (init) { + menu_click(false) + init = false + menu_click(false) + } + } + } + + await io.at(net.page.id) + is_called = true + return el + + // ---------------------------------------- + // FUNCTIONS + // ---------------------------------------- + function upload (e) { + const file = e.target.files[0] + const reader = new FileReader() + reader.onload = on_reader_load + + function on_reader_load (event) { + const content = event.target.result + try { + data = JSON.parse(content) + console.log(file) + if (current_data.radio.name === 'Names') { port.postMessage({ type: 'import_db', data: [data] }) } else { port.postMessage({ type: 'import_root', data: [data, file.name.split('.')[0]] }) } + } catch (err) { + console.error('Invalid JSON file', err) + } + } + + reader.readAsText(file) + } + function make_btn (name, classes, key, nesting) { + const btn = document.createElement('button') + if (select[nesting]) { + btn.innerHTML = ` + ${name} + ` + const input = btn.querySelector('input') + input.onchange = on_radio_input_change + + function on_radio_input_change () { radio_change(input) } + } else { btn.textContent = name } + btn.classList.add(...classes.split(' ')) + btn.setAttribute('tab', name.replaceAll(/[^A-Za-z0-9]/g, '')) + btn.setAttribute('key', key) + btn.setAttribute('title', name) + return btn + } + function make_tab (id, classes, sub_classes, nesting = 0) { + const tab = document.createElement('div') + tab.classList.add(...classes.split(' '), id.replaceAll(/[^A-Za-z0-9]/g, '')) + + let height + if (nesting % 2 === top_first) height = 565 - ((nesting + 1) * 30) + 'px' + else tab.style.maxWidth = 700 - ((nesting + 1) * 47) + 'px' + + tab.innerHTML = ` +
+
+
+
+ ` + + return tab + } + function make_textarea (id, classes, value, nesting) { + const textarea = document.createElement('textarea') + textarea.id = id.replaceAll(/[^A-Za-z0-9]/g, '') + textarea.classList.add(...classes.split(' ')) + textarea.value = typeof (value) === 'object' ? JSON.stringify(value, null, 2) : value + textarea.placeholder = 'Type here...' + textarea.style.width = 700 - ((nesting + 2) * 47) + 'px' + return textarea + } + function radio_change (radio) { + current_data.radio && (current_data.radio.checked = false) + current_data.radio = radio + } + async function menu_click (call) { + port = await item.get(net.page.id) + menu.classList.toggle('hidden') + if (init) { return } + init = true + + const old_box = menu.querySelector('.tab-content') + old_box && old_box.remove() + + const box = make_tab('any', 'tab-content active' + (top_first ? '' : ' sub'), ['btns', 'tabs']) + menu.append(box) + make_tabs(box, data, nesting) + } + function make_tabs (box, data, nesting) { + const local_nesting = nesting + 1 + const not_last_nest = local_nesting !== nesting_limit + let sub = '' + if (local_nesting % 2 === top_first) { sub = ' sub' } + const btns = box.querySelector('.btns') + const tabs = box.querySelector('.tabs') + Object.entries(data).forEach(create_tab_entry) + + function create_tab_entry (entry, i) { + const [key, value] = entry + let first = '' + if (!i) { + first = ' active' + current_data[labels[nesting]] = key + } + + const btn = make_btn(key, `tab-button${first}`, labels[nesting], nesting) + const tab = make_tab(key, `tab-content${sub + first}`, ['btns', 'tabs'], local_nesting) + btn.onclick = on_tab_button_click + + function on_tab_button_click () { tab_btn_click(btn, btns, tabs, '.root-tabs > .tab-content', 'node', key) } + + btns.append(btn) + tabs.append(tab) + if (typeof (value) === 'object' && value !== null && not_last_nest && Object.keys(value).length) { make_tabs(tab, value, local_nesting) } else { + const textarea = make_textarea(key, `subtab-textarea${first}`, value, local_nesting) + tab.append(textarea) + } + } + } + function tab_btn_click (btn, btns, tabs) { + btns.querySelector('.active').classList.remove('active') + tabs.querySelector(':scope > .active').classList.remove('active') + + btn.classList.add('active') + const tab = tabs.querySelector('.' + btn.getAttribute('tab')) + tab.classList.add('active') + current_data[btn.getAttribute('key')] = btn.textContent + + recurse(tab) + function recurse (tab) { + const btn = tab.querySelector('.btns > .active') + if (!btn) { return } + current_data[btn.getAttribute('key')] = btn.textContent + const sub_tab = tab.querySelector('.tabs > .active') + recurse(sub_tab) + } + } + + function apply () { + let raw = shadow.querySelector('.tab-content.active .tab-content.active textarea.active').value + if (current_data.Files.split('.')[1] === 'json') { raw = JSON.parse(raw) } + port.postMessage({ + type: 'put', + data: [ + current_data.dataset + current_data.file, + raw, + current_data.node + ] + }) + } + + function inject (data) { sheet.replaceSync(data[0]) } + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } +} + +function fallback_module () { + return { + api: fallback_instance + } + function fallback_instance () { + return { + drive: { + 'style/': { + 'quick_editor.css': { + raw: ` + .dots-button { + border: none; + font-size: 24px; + cursor: pointer; + line-height: 1; + background-color: white; + letter-spacing: 1px; + padding: 3px 5px; + border-radius: 20%; + box-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + + .quick-menu { + display: flex; + position: absolute; + top: 100%; + right: 0; + background: white; + padding: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + white-space: nowrap; + z-index: 10; + width: fit-content; + } + *{ + box-sizing: border-box; + } + + .hidden { + display: none; + } + + .btns::before { + display: none; + content: var(--before-content); + font-weight: bold; + color: white; + background: #4CAF50; + padding: 2px 6px; + border-radius: 4px; + position: absolute; + margin-left: -10px; + margin-top: -20px; + } + .btns:hover { + border: 2px solid #4CAF50; + } + .btns:hover::before { + display: block; + } + .btns{ + display: flex; + margin-bottom: 8px; + overflow-x: auto; + background: #d0f0d0; + } + .sub > .btns { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 400px; + overflow-y: auto; + min-width: fit-content; + margin-right: 8px; + background: #d0d2f0ff; + } + + .tab-button { + flex: 1; + padding: 6px; + background: #eee; + border: none; + cursor: pointer; + border-bottom: 2px solid transparent; + max-width: 70px; + width: fit-content; + text-overflow: ellipsis; + overflow: hidden; + min-width: 70px; + min-height: 29px; + position: relative; + text-align: left; + } + .tab-button.active { + background: #fff; + border-bottom: 2px solid #4CAF50; + } + .sub > div > .tab-button.active { + border-bottom: 2px solid #2196F3; + } + .tab-content { + display: none; + max-width: 700px; + background: #d0d2f0ff; + } + .tab-content.active { + display: block; + } + .tab-content.sub.active{ + display: flex; + align-items: flex-start; + } + + textarea { + width: 500px; + max-width: 560px; + height: 400px; + display: block; + resize: vertical; + } + + .button { + display: block; + margin-top: 10px; + padding: 5px 10px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + height: fit-content; + self-align: end; + width: 100%; + } + .btn-box { + border-right: 1px solid #ccc; + padding-right: 10px; + } + .tabs{ + border-left: 2px solid #ccc; + border-top: 1px solid #ccc; + } + button:has(input[type="radio"]:checked){ + background: #45abffff; + } + button > input[type="radio"]{ + width: 12px; + height: 12px; + border: 2px solid #555; + border-radius: 50%; + display: inline-block; + position: relative; + cursor: pointer; + margin: 0; + } + ` + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/quick_editor/quick_editor.js") +},{"STATE":1,"helpers":17}],25:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = steps_wizard + +async function steps_wizard (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + let currentActiveStep = 0 + let current_steps = [] + const click_state = { index: 0 } + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const steps_wizard_main = shadow.querySelector('.steps-wizard') + const steps_entries = shadow.querySelector('.steps-slot') + const select_step_action = { + name: 'Select Step', + info: 'Open the selected action step.', + icon: 'step', + status: { hidden: true }, + steps: [], + run: select_step + } + docs.register_actions([select_step_action]) + on_step_click.info = select_step_action.info + on_step_click.opts = { state: click_state } + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + // docs_toggle handled globally by DOCS module + if (type === 'init_data' && data) { + render_steps(data, true) + } + } + } + + function render_steps (steps, auto_focus_first) { + if (!steps) { return } + current_steps = steps + + const is_single_step = steps.length === 1 + steps_wizard_main.style.display = is_single_step ? 'none' : '' + + steps_entries.innerHTML = '' + currentActiveStep = 0 + + steps.forEach(create_step_button) + + function create_step_button (step, index) { + const btn = document.createElement('button') + btn.className = 'step-button' + btn.textContent = step.name + (step.type === 'optional' ? ' *' : '') + btn.title = btn.textContent + btn.setAttribute('data-step', index + 1) + + const accessible = can_access(index, steps) + + let status = 'default' + if (!accessible) status = 'disabled' + else if (step.is_completed) status = 'completed' + else if (step.status === 'error') status = 'error' + else if (step.type === 'optional') status = 'optional' + + btn.classList.add(`step-${status}`) + + if (index === currentActiveStep - 1 && index > 0) { + btn.classList.add('back') + } + if (index === currentActiveStep + 1 && index < steps.length - 1) { + btn.classList.add('next') + } + if (index === currentActiveStep) { + btn.classList.add('active') + } + + btn.onclick = docs.wrap_isolated(on_step_click) + + steps_entries.appendChild(btn) + + if (auto_focus_first && index === 0) { + btn.classList.add('active') + center_step(btn) + _.up('step_clicked', {}, { ...step, index: 0, total_steps: steps.length, is_accessible: accessible }) + } + } + } + + function on_step_click (event, $) { + $.state.index = Number(event.currentTarget.dataset.step) - 1 + $('Select Step') + } + + function select_step () { + const index = click_state.index + const step = current_steps[index] + const accessible = can_access(index, current_steps) + currentActiveStep = index + center_step(steps_entries.children[index]) + render_steps(current_steps, false) + _.up('step_clicked', {}, { ...step, index, total_steps: current_steps.length, is_accessible: accessible }) + } + + function center_step (step_button) { + const container_width = steps_entries.clientWidth + const step_left = step_button.offsetLeft + const step_width = step_button.offsetWidth + + const center_position = step_left - (container_width / 2) + (step_width / 2) + + steps_entries.scrollTo({ + left: center_position, + behavior: 'smooth' + }) + } + + function can_access (index, steps) { + for (let i = 0; i < index; i++) { + if (!steps[i].is_completed && steps[i].type !== 'optional') { + return false + } + } + + return true + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'stepswizard.css': { + $ref: 'stepswizard.css' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/steps_wizard/steps_wizard.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],26:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const tabs = require('tabs') +const program_container = require('program_container') +const action_bar = require('action_bar') +const action_executor = require('action_executor') + +module.exports = tab_group + +async function tab_group (opts, invite) { + console.error('tab_group: initializing with opts', opts.sid) + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+
+
+ ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const tab_group_container = shadow.querySelector('.tab-group') + const program_container_slot = shadow.querySelector('.program-container-slot') + const action_executor_slot = shadow.querySelector('.action-executor-slot') + const action_bar_slot = shadow.querySelector('.action-bar-slot') + const tabs_slot = shadow.querySelector('.tabs-slot') + + const subs = await sdb.watch(onbatch) + console.error('tab_group: subs ready', subs) + + let tabs_el = null + let program_container_el = null + let action_bar_el = null + let action_executor_el = null + + io.on = { + up: io_up(), + tabs: io_tabs(), + program_container: io_program_container(), + action_bar: io_action_bar(), + action_executor: io_action_executor() + } + if (invite) { + console.error('tab_group: accepting invite') + io.accept(invite) + } + + console.error('tab_group: creating program_container') + program_container_el = await program_container({ ...subs[0], ids: { up: id } }, io.invite('program_container', { up: id })) + program_container_el.classList.add('program-container') + program_container_slot.replaceWith(program_container_el) + console.error('tab_group: program_container created') + + console.error('tab_group: creating action_executor') + action_executor_el = await action_executor({ ...subs[1], ids: { up: id } }, io.invite('action_executor', { up: id })) + action_executor_el.classList.add('action-executor') + action_executor_slot.replaceWith(action_executor_el) + console.error('tab_group: action_executor created') + + console.error('tab_group: creating tabs') + tabs_el = await tabs({ ...subs[2], ids: { up: id } }, io.invite('tabs', { up: id })) + tabs_el.classList.add('tabs') + tabs_slot.replaceWith(tabs_el) + console.error('tab_group: tabs created') + + console.error('tab_group: creating action_bar') + action_bar_el = await action_bar({ ...subs[3], ids: { up: id } }, io.invite('action_bar', { up: id })) + action_bar_el.classList.add('action-bar') + action_bar_slot.replaceWith(action_bar_el) + console.error('tab_group: action_bar created') + + console.error('tab_group: initialization complete') + return el + + // --------------------------- + // BATCH HANDLER + // --------------------------- + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + function fail ({ data, type }) { console.error('tab_group: unhandled batch message', { type, data }) } + + // --------------------------- + // MESSAGE FROM ROOT (tile_manager) + // --------------------------- + + function io_up () { + const on = { + update_quick_actions_for_app, + update_steps_wizard_for_app, + update_actions_for_app: forward_to_program_container, + load_actions: forward_to_program_container, + create_default_tab, + show_collapsed_tab_group, + hide_collapsed_tab_group, + tile_focus_changed + } + return function onmessage (msg) { + console.error('tab_group: message from root', msg.type) + ;(on[msg.type] || onfail)(msg) + } + function update_quick_actions_for_app (msg) { + console.error('tab_group: forwarding to action_bar', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.(msg.type, refs, msg.data) + } + function update_steps_wizard_for_app (msg) { + console.error('tab_group: forwarding to action_executor', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.action_executor?.(msg.type, refs, msg.data) + } + function forward_to_program_container (msg) { + console.error('tab_group: forwarding to program_container', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.program_container?.(msg.type, refs, msg.data) + } + function create_default_tab (msg) { + console.error('tab_group: creating default tab', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('add_default_tab', refs, msg.data) + } + function show_collapsed_tab_group (msg) { + console.error('tab_group: showing collapsed tab group', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('show_collapsed_tab_group', refs, msg.data) + } + function hide_collapsed_tab_group (msg) { + console.error('tab_group: hiding collapsed tab group', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('hide_collapsed_tab_group', refs, msg.data) + } + function tile_focus_changed (msg) { + console.error('tab_group: tile focus changed', msg.data) + const { is_focused } = msg.data + tab_group_container.classList.toggle('tile-focused', is_focused) + // Forward to tabs so active tab styling can update + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('tile_focus_changed', refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unknown root message', msg) } + } + + // --------------------------- + // PROTOCOLS + // --------------------------- + + function io_tabs () { + return function onmessage (msg) { + console.error('tab_group: tabs_protocol', msg.type, msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + } + + function io_program_container () { + const forward_up = { + ui_focus: forward, + action_auto_completed: forward, + action_complete: forward + } + return function onmessage (msg) { + console.error('tab_group: program_container_protocol', msg.type) + ;(forward_up[msg.type] || onfail)(msg) + } + function forward (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled program_container msg', msg) } + } + + function io_action_bar () { + const forward_to_executor = { + action_submitted: to_executor, + selected_action: to_executor, + activate_steps_wizard: to_executor, + render_form: to_executor, + clean_up: to_executor + } + const forward_up = { + ui_focus: to_up, + display_actions: to_up, + filter_actions: to_up, + console_history_toggle: to_up + } + return function onmessage (msg) { + console.error('tab_group: action_bar_protocol', msg.type) + const handler = forward_to_executor[msg.type] || forward_up[msg.type] || onfail + handler(msg) + } + function to_executor (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_executor?.(msg.type, refs, msg.data) + } + function to_up (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled action_bar msg', msg) } + } + + function io_action_executor () { + const forward_to_action_bar = { + selected_action: to_action_bar, + show_submit_btn: to_action_bar, + hide_submit_btn: to_action_bar, + update_quick_actions_input: to_action_bar, + step_clicked: to_action_bar, + load_actions: to_action_bar + } + const forward_up = { + action_auto_completed: to_up_and_bar, + action_complete: to_up + } + return function onmessage (msg) { + console.error('tab_group: action_executor_protocol', msg.type) + const handler = forward_to_action_bar[msg.type] || forward_up[msg.type] || onfail + handler(msg) + } + function to_action_bar (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.(msg.type, refs, msg.data) + } + function to_up (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function to_up_and_bar (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.('action_submitted', refs, msg.data) + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled action_executor msg', msg) } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + tabs: { $: '' }, + program_container: { $: '' }, + action_bar: { $: '' }, + action_executor: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + program_container: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs_style: 'docs_style', + docs: 'docs' + } + }, + action_executor: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + data: 'data', + hardcons: 'hardcons', + variables: 'variables' + } + }, + tabs: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + docs: 'docs', + variables: 'variables' + } + }, + action_bar: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + hardcons: 'hardcons', + prefs: 'prefs', + variables: 'variables', + data: 'data' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'tab_group.css': { + $ref: 'style/tab_group.css' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/tab_group/tab_group.js") +},{"STATE":1,"action_bar":7,"action_executor":8,"net_helper":20,"program_container":22,"tabs":28}],27:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = tabbed_editor + +async function tabbed_editor (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + files: onfiles, + active_tab: onactivetab + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
Select a file to edit
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const editor_content = shadow.querySelector('.editor-content') + + let init = false + let files = {} + let active_tab = null + let current_editor = null + const on_message = { + switch_tab: handle_switch_tab, + close_tab: handle_close_tab, + toggle_tab: handle_toggle_tab + } + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_switch_tab (msg) { switch_to_tab(msg.data, msg) } + function handle_close_tab (msg) { + const tab_data = msg.data + if (active_tab === tab_data.id) { + hide_editor() + active_tab = null + } + + _.up('tab_closed', msg.head ? { cause: msg.head } : {}, tab_data) + } + function handle_toggle_tab (msg) { + const tab_data = msg.data + if (active_tab === tab_data.id) { + hide_editor() + active_tab = null + } else { + switch_to_tab(tab_data, msg) + } + } + function onmessage_fail () { /* docs_toggle @TODO */ } + + function switch_to_tab (tab_data, msg) { + if (active_tab === tab_data.id) { + return + } + + active_tab = tab_data.id + create_editor(tab_data) + _.up('tab_switched', msg?.head ? { cause: msg.head } : {}, tab_data) + } + + function create_editor (tab_data) { + const parsed_data = JSON.parse(tab_data[0]) + const file_content = files[parsed_data.id] || '' + // console.log('Creating editor for:', parsed_data) + + editor_content.replaceChildren() + + editor_content.innerHTML = ` +
+
+
+ +
+
` + const editor = editor_content.querySelector('.code-editor') + const line_numbers = editor_content.querySelector('.line-numbers') + const code_area = editor_content.querySelector('.code-area') + current_editor = { editor, code_area, line_numbers, tab_data: parsed_data } + + code_area.oninput = handle_code_input + code_area.onscroll = handle_code_scroll + + update_line_numbers() + } + + function hide_editor () { + editor_content.innerHTML = ` +
+
Select a file to edit
+
` + current_editor = null + } + + function update_line_numbers () { + if (!current_editor) return + + const { code_area, line_numbers } = current_editor + const lines = code_area.value.split('\n') + const line_count = lines.length + + let line_html = '' + for (let i = 1; i <= line_count; i++) { + line_html += `
${i}
` + } + + line_numbers.innerHTML = line_html + } + + function save_file_content () { + if (!current_editor) return + + const { code_area, tab_data } = current_editor + files[tab_data.id] = code_area.value + _.up('file_changed', {}, { + id: tab_data.id, + content: code_area.value + }) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + init = true + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('Invalid message', { data, type }) } + function inject (data) { sheet.replaceSync(data[0]) } + function onfiles (data) { files = data[0] } + + function onactivetab (data) { + if (data.id !== active_tab) { + switch_to_tab(data) + } + } + + function handle_code_input () { + update_line_numbers() + save_file_content() + } + + function handle_code_scroll () { + if (!current_editor) return + const { code_area, line_numbers } = current_editor + line_numbers.scrollTop = code_area.scrollTop + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'files/': { + 'example.js': { + raw: ` + function hello() { + console.log("Hello, World!"); + } + + const x = 42; + let y = "string"; + + if (x > 0) { + hello(); + } + ` + }, + 'example.md': { + raw: ` + # Example Markdown + This is an **example** markdown file. + + ## Features + + - Syntax highlighting + - Line numbers + - File editing + + \`\`\`javascript + function example() { + return true; + } + \`\`\` + ` + }, + 'data.json': { + raw: ` + { + "name": "example", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.21" + } + ` + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .tabbed-editor { + width: 100%; + height: 100%; + min-height: 80px; + background-color: #0d1117; + color: #e6edf3; + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; + display: grid; + grid-template-rows: 1fr; + position: relative; + border: 1px solid #30363d; + border-radius: 6px; + box-sizing: border-box; + overflow: hidden; + } + + .editor-content { + display: grid; + grid-template-rows: 1fr; + min-height: 0; + position: relative; + overflow: hidden; + background-color: #0d1117; + } + + .editor-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #7d8590; + font-style: italic; + font-size: 16px; + background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); + } + + .code-editor { + height: 100%; + min-height: 0; + display: grid; + grid-template-rows: 1fr; + background-color: #0d1117; + } + + .editor-wrapper { + display: grid; + grid-template-columns: auto 1fr; + min-height: 0; + height: 100%; + position: relative; + box-sizing: border-box; + overflow: auto; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + background-color: #0d1117; + } + + .line-numbers { + background-color: #161b22; + color: #7d8590; + padding: 12px 16px; + text-align: right; + user-select: none; + font-size: 13px; + line-height: 20px; + font-weight: 400; + border-right: 1px solid #21262d; + position: sticky; + left: 0; + z-index: 1; + height: 100%; + } + + .line-number { + height: 20px; + line-height: 20px; + transition: color 0.1s ease; + } + + .line-number:hover { + color: #f0f6fc; + } + + .code-area { + background-color: #0d1117; + color: #e6edf3; + border: none; + outline: none; + resize: none; + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; + font-size: 13px; + line-height: 20px; + padding: 12px 16px; + position: relative; + z-index: 2; + tab-size: 2; + white-space: pre; + overflow-wrap: normal; + overflow-x: auto; + } + + .code-area:focus { + background-color: #0d1117; + box-shadow: none; + } + + .code-area::selection { + background-color: #264f78; + } + + .editor-wrapper::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + .editor-wrapper::-webkit-scrollbar-track { + background: transparent; + } + + .editor-wrapper::-webkit-scrollbar-thumb { + background: #30363d; + border: 2px solid transparent; + border-radius: 999px; + background-clip: content-box; + } + + .editor-wrapper::-webkit-scrollbar-thumb:hover { + background: #484f58; + border: 2px solid transparent; + background-clip: content-box; + } + ` + } + }, + 'active_tab/': { + 'current.json': { + raw: JSON.stringify({ + id: 'example.js', + name: 'example.js' + }) + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/tabbed_editor/tabbed_editor.js") +},{"STATE":1,"net_helper":20}],28:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = component + +async function component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + variables: onvariables, + style: inject, + icons: iconject, + scroll: onscroll + } + const div = document.createElement('div') + const shadow = div.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
` + const entries = shadow.querySelector('.tab-entries') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + let init = false + let variables = [] + let dricons = [] + let active = null + let tab_group_el = null + let nested_collapse_el = null + const expanded_groups = {} + let current_collapse_data = null + let tab_group_expanded = false + let tile_is_focused = false + let docs_action_id = 0 + const default_tabs = {} + const link_tabs = {} + const variable_tabs = {} + let ARROW_LEFT_SVG = '' + let ARROW_RIGHT_SVG = '' + let SEPARATOR_SVG = '' + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + + const actions_file = await drive.get('actions/commands.json') + const docs_actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + docs.register_actions(docs_actions) + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + if (entries) { + let is_down = false + let start_x + let scroll_start + + function stop_drag_scroll () { + is_down = false + entries.classList.remove('grabbing') + update_scroll_position() + } + + function move_drag_scroll (pointer_x) { + if (!is_down) return + if (entries.scrollWidth <= entries.clientWidth) return stop_drag_scroll() + entries.scrollLeft = scroll_start - (pointer_x - start_x) * 1.5 + } + + entries.onmousedown = on_entries_mousedown + + function on_entries_mousedown (e) { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + entries.classList.add('grabbing') + start_x = e.pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + window.onmousemove = on_window_mousemove + window.onmouseup = on_window_mouseup + } + + function on_window_mousemove (e) { + move_drag_scroll(e.pageX - entries.offsetLeft) + e.preventDefault() + } + + function on_window_mouseup () { + stop_drag_scroll() + window.onmousemove = null + window.onmouseup = null + } + + entries.onmouseleave = stop_drag_scroll + entries.ontouchstart = on_entries_touchstart + + function on_entries_touchstart (e) { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + start_x = e.touches[0].pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + } + + ;['ontouchend', 'ontouchcancel'].forEach(bind_touch_end_handler) + entries.ontouchmove = on_entries_touchmove + + function bind_touch_end_handler (event_name) { entries[event_name] = stop_drag_scroll } + function on_entries_touchmove (e) { + move_drag_scroll(e.touches[0].pageX - entries.offsetLeft) + e.preventDefault() + } + } + return div + + function io_up () { + const on = { + add_link_tab: handle_add_link_tab, + remove_link_tab: handle_remove_link_tab, + add_default_tab: handle_add_default_tab, + restore_tab: handle_restore_tab, + show_collapsed_tab_group: handle_show_collapsed_tab_group, + hide_collapsed_tab_group: handle_hide_collapsed_tab_group, + tile_focus_changed: handle_tile_focus_changed, + sync_tab_count: handle_sync_tab_count + } + return function onmessage (msg) { + console.error('tabs: message from up', msg) + ;(on[msg.type] || onfail)(msg) + } + function handle_add_link_tab ({ data }) { add_link_tab(data) } + function handle_remove_link_tab ({ data }) { remove_link_tab(data) } + function handle_add_default_tab ({ data }) { add_default_tab(data) } + function handle_restore_tab ({ data }) { + create_btn({ name: data.name, id: data.id }) + sync_tab_count() + } + function handle_show_collapsed_tab_group ({ data }) { show_collapsed_tab_group(data) } + function handle_hide_collapsed_tab_group () { hide_collapsed_tab_group() } + function handle_tile_focus_changed ({ data }) { update_tab_focus_state(data) } + function handle_sync_tab_count () { sync_tab_count() } + function onfail (msg) { console.error('tabs: unknown message', msg) } + } + + function add_default_tab ({ name, program, tile_id, id }) { + const tab_id = id || 'tab_' + Date.now() + if (default_tabs[tab_id]) { + console.error('tabs: default tab already exists', tab_id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + ${dricons[1] || '📄'} + ${tab_id} + ${name || 'New Tab'} + ` + el.className = 'tabsbtn default-tab active' + const name_el = el.querySelector('.name') + const close_btn = el.querySelector('.btn') + default_tabs[tab_id] = { el, name_el, close_btn, name, program } + if (active) active.classList.remove('active') + active = el + name_el.onclick = switch_active + close_btn.onclick = close_tab + entries.appendChild(el) + console.error('tabs: default tab added', tab_id) + sync_tab_count() + sync_separators() + + function switch_active () { + console.error('tabs: default tab clicked', tab_id) + if (active) active.classList.remove('active') + el.classList.add('active') + active = el + const data = { id: tab_id, name, program } + _.up('ui_focus', {}, { type: 'tab', sid: opts.sid }) + _.up('tab_name_clicked', {}, data) + } + function close_tab (e) { + e.stopPropagation() + console.error('tabs: default tab close clicked', tab_id) + el.remove() + delete default_tabs[tab_id] + if (active === el) active = null + _.up('tab_close_clicked', {}, { id: tab_id, name }) + sync_tab_count() + if (Object.keys(default_tabs).length === 0) { + _.up('all_tabs_closed', {}, null) + } + sync_separators() + } + } + + function add_link_tab ({ tile_id, name, direction }) { + const link_tab_id = 'split_tile_' + tile_id + if (link_tabs[link_tab_id]) { + console.error('tabs: link tab already exists', link_tab_id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + + ${name || 'Split ' + direction}` + el.className = 'tabsbtn link-tab' + const name_el = el.querySelector('.name') + link_tabs[link_tab_id] = { el, name_el, tile_id, name, direction } + el.onclick = on_link_tab_click + entries.appendChild(el) + console.error('tabs: link tab added', link_tab_id) + sync_separators() + + function on_link_tab_click () { + console.error('tabs: link tab clicked', link_tab_id) + _.up('link_tab_clicked', {}, { tile_id, link_tab_id }) + } + } + + function remove_link_tab ({ tile_id }) { + const link_tab_id = 'split_tile_' + tile_id + const tab = link_tabs[link_tab_id] + if (tab) { + tab.el.remove() + delete link_tabs[link_tab_id] + console.error('tabs: link tab removed', link_tab_id) + sync_separators() + } + } + + function show_collapsed_tab_group (data) { + if (data.nested) { + show_nested_collapse(data) + return + } + + const { tiles } = data + if (tab_group_el) hide_collapsed_tab_group() + + tab_group_el = document.createElement('div') + tab_group_el.className = 'tab-group-inline' + + const group_tab = document.createElement('div') + group_tab.className = 'tabsbtn tab-group-tab' + const direction = (tiles[0] && tiles[0].direction) || 'right' + const icon_svg = (direction === 'left' || direction === 'up') ? ARROW_LEFT_SVG : ARROW_RIGHT_SVG + group_tab.innerHTML = `${icon_svg}Split` + group_tab.onclick = toggle_tab_group_expand + tab_group_el.appendChild(group_tab) + + const children_container = document.createElement('div') + children_container.className = 'tab-group-children' + + for (const tile of tiles) { + const tile_tab_list = tile.tabs || [] + for (const tab of tile_tab_list) { + const tab_el = document.createElement('div') + tab_el.className = 'tabsbtn tab-group-child' + tab_el.innerHTML = `${dricons[1] || '📄'}${tab.name || 'Tab'}` + tab_el.onclick = on_child_tab_click(tile.tile_id) + children_container.appendChild(tab_el) + } + if (tile_tab_list.length === 0) { + const placeholder_el = document.createElement('div') + placeholder_el.className = 'tabsbtn tab-group-child' + placeholder_el.innerHTML = `${dricons[1] || '📄'}New Tab` + placeholder_el.onclick = on_child_tab_click(tile.tile_id) + children_container.appendChild(placeholder_el) + } + } + + tab_group_el.appendChild(children_container) + entries.insertBefore(tab_group_el, entries.firstChild) + console.error('tabs: collapsed tab group shown') + sync_separators() + + function toggle_tab_group_expand () { + tab_group_expanded = !tab_group_expanded + tab_group_el.classList.toggle('expanded', tab_group_expanded) + sync_separators() + } + + function on_child_tab_click (tile_id) { + return function () { + console.error('tabs: tab group child clicked, requesting expand', tile_id) + _.up('tab_group_tile_clicked', {}, { tile_id }) + } + } + } + + function hide_collapsed_tab_group () { + if (tab_group_el) { + tab_group_el.remove() + tab_group_el = null + tab_group_expanded = false + console.error('tabs: collapsed tab group hidden') + } + if (nested_collapse_el) { + nested_collapse_el.remove() + nested_collapse_el = null + console.error('tabs: nested collapse hidden') + } + sync_separators() + } + + function show_nested_collapse (data) { + console.error('tabs: show_nested_collapse', data) + current_collapse_data = data + if (tab_group_el) { + tab_group_el.remove() + tab_group_el = null + } + if (nested_collapse_el) { + nested_collapse_el.remove() + nested_collapse_el = null + } + + const { collapsed_groups } = data + + if (!collapsed_groups || collapsed_groups.length === 0) return + + nested_collapse_el = document.createElement('div') + nested_collapse_el.className = 'nested-collapse-container' + + function get_first_leaf_node (node) { + if (!node) return null + if (node.type === 'leaf') return node + if (node.children && node.children.length > 0) { + return get_first_leaf_node(node.children[0]) + } + return null + } + + // `depth` is the nesting depth in the collapsed strip, it drives the size ramp in CSS. + function render_leaf_tab (lt, leaf, depth, is_header = false, is_expanded = false, parent_group = null) { + const el = document.createElement('div') + el.className = `tabsbtn nested-leaf-tab depth-${Math.min(depth + 1, 3)}` + if (active && active.querySelector('.name')?.textContent === (lt.name || 'Tab')) { + el.classList.add('active') + } + + let arrow_btn_html = '' + if (is_header && parent_group) { + const icon_svg = is_expanded ? ARROW_LEFT_SVG : ARROW_RIGHT_SVG + arrow_btn_html = `${icon_svg}` + } + + // A folded tab is a pointer to another tile, not an owned tab, so it has no close + // button: closing stays with the tile that owns the tab. + el.innerHTML = ` + ${arrow_btn_html} + ${dricons[1] || '📄'} + ${lt.name || 'Tab'}` + + if (is_header && parent_group) { + const arrow_btn = el.querySelector('.group-arrow-btn') + if (arrow_btn) { + arrow_btn.onclick = (e) => { + e.stopPropagation() + expanded_groups[parent_group.split_id] = !is_expanded + show_nested_collapse(current_collapse_data) + } + } + } + + const name_el = el.querySelector('.name') + if (name_el) { + name_el.onclick = on_folded_tab_click + } + + return el + + // Clicking a folded tab asks the tile manager to bring that tile back on screen. + function on_folded_tab_click () { + if (active) active.classList.remove('active') + el.classList.add('active') + active = el + _.up('ui_focus', {}, { type: 'tab', sid: opts.sid }) + _.up('tab_group_tile_clicked', {}, { tile_id: leaf.tile_id, id: lt.id, name: lt.name }) + } + } + + function render_node_to_dom (node, depth, is_header_child = false, parent_group = null) { + if (!node) return null + + if (node.type === 'leaf') { + const leaf_container = document.createElement('div') + leaf_container.className = 'nested-group-leaf' + leaf_container.setAttribute('data-depth', depth) + + let leaf_tabs = node.tabs || [] + if (leaf_tabs.length === 0) { + leaf_tabs = [{ id: 'placeholder', name: 'New Tab', tile_id: node.tile_id }] + } + + if (is_header_child && parent_group) { + const is_expanded = !!expanded_groups[parent_group.split_id] + const header_tab = render_leaf_tab(leaf_tabs[0], node, depth, true, is_expanded, parent_group) + leaf_container.appendChild(header_tab) + + if (is_expanded) { + for (let i = 1; i < leaf_tabs.length; i++) { + const normal_tab = render_leaf_tab(leaf_tabs[i], node, depth, false, false, null) + leaf_container.appendChild(normal_tab) + } + } + } else { + for (const lt of leaf_tabs) { + const normal_tab = render_leaf_tab(lt, node, depth, false, false, null) + leaf_container.appendChild(normal_tab) + } + } + + return leaf_container + } + + if (node.type === 'collapsed_group' || node.type === 'split_group') { + const is_expanded = !!expanded_groups[node.split_id] + + if (!is_expanded) { + const first_leaf = get_first_leaf_node(node) + if (!first_leaf) return null + + let leaf_tabs = first_leaf.tabs || [] + if (leaf_tabs.length === 0) { + leaf_tabs = [{ id: 'placeholder', name: 'New Tab', tile_id: first_leaf.tile_id }] + } + + const leaf_container = document.createElement('div') + leaf_container.className = 'nested-group-leaf' + leaf_container.setAttribute('data-depth', depth) + + const header_tab = render_leaf_tab(leaf_tabs[0], first_leaf, depth, true, false, node) + leaf_container.appendChild(header_tab) + return leaf_container + } else { + const group_container = document.createElement('div') + group_container.className = `nested-group-container depth-${depth} expanded` + group_container.setAttribute('data-depth', depth) + + if (node.children && node.children.length > 0) { + const first_child_el = render_node_to_dom(node.children[0], depth, true, node) + if (first_child_el) group_container.appendChild(first_child_el) + + for (let i = 1; i < node.children.length; i++) { + const child_el = render_node_to_dom(node.children[i], depth + 1, false, null) + if (child_el) group_container.appendChild(child_el) + } + } + + return group_container + } + } + + return null + } + + for (const group of collapsed_groups) { + const group_el = render_node_to_dom(group, 0, false, null) + if (group_el) nested_collapse_el.appendChild(group_el) + } + + entries.insertBefore(nested_collapse_el, entries.firstChild) + console.error('tabs: nested collapse shown with', collapsed_groups.length, 'groups') + sync_separators() + } + + function update_tab_focus_state ({ is_focused }) { + entries.classList.toggle('tile-focused', is_focused) + entries.classList.toggle('tile-inactive', !is_focused) + } + + function get_tab_count () { + return Object.keys(default_tabs).length + Object.keys(variable_tabs).length + } + + function sync_tab_count () { + _.up('update_tab_count', {}, { count: get_tab_count() }) + } + + async function create_btn ({ name, id }, index = 0) { + if (variable_tabs[id]) { + console.error('tabs: variable tab already exists', id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + ${dricons[index + 1] || dricons[1] || '📄'} + ${id} + ${name} + ` + + el.className = 'tabsbtn' + const name_el = el.querySelector('.name') + const close_btn = el.querySelector('.btn') + + name_el.draggable = false + + const action_id = ++docs_action_id + const open_action = create_action('Open Tab ' + action_id, 'Open the ' + name + ' tab.', 'tab', on_tab_name_click) + const close_action = create_action('Close Tab ' + action_id, 'Close the ' + name + ' tab.', 'close', on_tab_close_click) + on_name_click.info = open_action.info + on_name_click.opts = { state: { action: open_action.name } } + on_close_click.info = close_action.info + on_close_click.opts = { state: { action: close_action.name } } + name_el.onclick = docs.wrap_isolated(on_name_click) + close_btn.onclick = docs.wrap_isolated(on_close_click) + variable_tabs[id] = { el, name, actions: [open_action, close_action] } + register_actions() + + function on_name_click (event, $) { $($.state.action) } + function on_close_click (event, $) { + event.stopPropagation() + $($.state.action) + } + function on_tab_name_click () { + const data = { type: 'tab', sid: opts.sid } + _.up('ui_focus', {}, data) + _.up('tab_name_clicked', {}, { id, name }) + } + function on_tab_close_click () { + el.remove() + delete variable_tabs[id] + register_actions() + const data = { type: 'tab', sid: opts.sid } + _.up('ui_focus', {}, data) + _.up('tab_close_clicked', {}, { id, name }) + sync_tab_count() + } + entries.appendChild(el) + sync_separators() + } + + function create_action (name, info, icon, run) { + return { name, info, icon, status: { hidden: true }, steps: [], run } + } + + function register_actions () { + const tab_actions = Object.values(variable_tabs).flatMap(tab => tab.actions) + docs.register_actions(docs_actions.concat(tab_actions)) + } + + function create_separator () { + const sep = document.createElement('div') + sep.className = 'tab-separator' + sep.innerHTML = ` + + + + ` + return sep + } + + function sync_separators () { + const existing_seps = entries.querySelectorAll(':scope > .tab-separator') + for (const sep of existing_seps) { + sep.remove() + } + + const main_children = Array.from(entries.children).filter(child => { + if (child.style.display === 'none') return false + return child.classList.contains('tabsbtn') || + child.classList.contains('nested-collapse-container') || + child.classList.contains('tab-group-inline') + }) + + for (let i = 0; i < main_children.length - 1; i++) { + const sep = create_separator() + entries.insertBefore(sep, main_children[i].nextSibling) + } + + if (nested_collapse_el) { + const group_children = Array.from(nested_collapse_el.children).filter(child => { + if (child.style.display === 'none') return false + return child.classList.contains('nested-group-container') || + child.classList.contains('nested-group-leaf') + }) + + const existing_nested_seps = nested_collapse_el.querySelectorAll(':scope > .tab-separator') + for (const sep of existing_nested_seps) { + sep.remove() + } + + for (let i = 0; i < group_children.length - 1; i++) { + const sep = create_separator() + nested_collapse_el.insertBefore(sep, group_children[i].nextSibling) + } + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + if (!opts.ids) variables.forEach(create_btn) + init = true + } else { + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + + function onvariables (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + variables = vars + } + + function iconject (data) { + dricons = data + ARROW_LEFT_SVG = data[4] || '' + ARROW_RIGHT_SVG = data[5] || '' + SEPARATOR_SVG = data[6] || '' + } + + function update_scroll_position () { + } + + function onscroll (data) { + setTimeout(apply_scroll_position, 200) + function apply_scroll_position () { + if (entries) { + entries.scrollLeft = data + } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + 'cross.svg': { + $ref: 'cross.svg' + }, + '1.svg': { + $ref: 'icon.svg' + }, + '2.svg': { + $ref: 'icon.svg' + }, + '3.svg': { + $ref: 'icon.svg' + }, + '4.svg': { + $ref: 'arrow-left.svg' + }, + '5.svg': { + $ref: 'arrow-right.svg' + }, + '6.svg': { + $ref: 'separator.svg' + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'New Tab', + info: 'Create a new tab in the current tab strip.', + icon: 'plus', + status: { + pinned: true, + default: true + }, + steps: [ + { name: 'Enter Tab Name', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Duplicate Tab', + info: 'Copy an existing tab and open the duplicate as a new tab.', + icon: 'copy', + status: { + pinned: false, + default: false + }, + steps: [ + { name: 'Select Tab to Duplicate', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Enter New Tab Name', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Close Tab', + info: 'Close the selected tab after confirmation.', + icon: 'close', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Tab to Close', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Close', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'variables/': { + 'tabs.json': { + $ref: 'tabs.json' + } + }, + 'scroll/': { + 'position.json': { + raw: '100' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + $ref: 'style.css' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/tabs/tabs.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],29:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const state_db = STATE(__filename) +const { get } = state_db(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +const tabs_component = require('tabs') +const task_manager = require('task_manager') + +module.exports = tabsbar + +async function tabsbar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: inject_icons + } + + let dricons = {} + let docs_toggle_active = false + const on_message = { + docs_toggle: handle_docs_toggle, + add_link_tab: handle_forward_tabs, + remove_link_tab: handle_forward_tabs, + show_collapsed_tab_group: handle_forward_tabs, + hide_collapsed_tab_group: handle_forward_tabs, + tile_focus_changed: handle_forward_tabs, + restore_tab: handle_forward_tabs, + } + const { io, _ } = net(id) + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + const docs = DOCS(__filename)(opts.sid) + const actions_file = await drive.get('actions/command.json') + const actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + const focus_hat_action = { + name: 'Focus Wizard Hat', + info: 'Focus the wizard hat.', + icon: 'hat', + status: { hidden: true }, + steps: [], + run: focus_wizard_hat + } + docs.register_actions(actions.concat(focus_hat_action)) + on_hat_click.info = focus_hat_action.info + + io.on = { + up: io_up(), + tabs: io_tabs(), + task_manager: io_task_manager() + } + if (invite) { + io.accept(invite) + const data = { + type: 'wizard_hat', + sid: opts.sid + } + _.up('ui_focus', {}, data) + } + + shadow.innerHTML = ` +
+ + + + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const hat_btn = shadow.querySelector('.hat-btn') + const bar_btn = shadow.querySelector('.bar-btn') + + const subs = await sdb.watch(onbatch) + + function onload (svg) { + const parser = new DOMParser() + const doc = parser.parseFromString(svg, 'image/svg+xml') + const svgElem = doc.documentElement + hat_btn.replaceChildren(svgElem) + hat_btn.onclick = docs.wrap_isolated(on_hat_click) + } + if (dricons[0]) { + onload(dricons[0]) + } + if (dricons[2]) { + const parser = new DOMParser() + const doc = parser.parseFromString(dricons[2], 'image/svg+xml') + const svgElem = doc.documentElement + bar_btn.replaceChildren(svgElem) + bar_btn.onclick = on_bar_btn_click + + function on_bar_btn_click () { + docs_toggle_active = !docs_toggle_active + // Send message to root module to set docs mode + _.up('set_docs_mode', {}, { active: docs_toggle_active }) + // Also send docs_toggle notification for UI updates + _.up('docs_toggle', {}, { active: docs_toggle_active }) + bar_btn.classList.toggle('active', docs_toggle_active) + _.task_manager('docs_toggle', {}, { active: docs_toggle_active }) + } + } + const tabs = await tabs_component({ ...subs[0] }, io.invite('tabs', { up: id })) + tabs.classList.add('tabs-bar') + shadow.querySelector('tabs').replaceWith(tabs) + + const task_mgr = await task_manager({ ...subs[1] }, io.invite('task_manager', { up: id })) + task_mgr.classList.add('bar-btn') + shadow.querySelector('task-manager').replaceWith(task_mgr) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function on_hat_click (event, $) { $('Focus Wizard Hat') } + function focus_wizard_hat () { _.up('ui_focus', {}, { type: 'wizard_hat', sid: opts.sid }) } + + function handle_docs_toggle (msg) { _.tabs(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_forward_tabs (msg) { _.tabs(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function onmessage_fail () { + // Handle other message types + } + + function io_tabs () { + return function tabs_protocol (msg) { + const action_handlers = { + update_tab_count: tabs_update_tab_count + } + const handler = action_handlers[msg.type] || tabs_forward_up + handler(msg) + + function tabs_update_tab_count (msg) { _.task_manager(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function tabs_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_task_manager () { + return function task_manager_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function inject_icons (data) { dricons = data } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + tabs: { + $: '' + }, + task_manager: { + $: '' + }, + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + tabs: { + 0: '', + mapping: { + icons: 'icons', + variables: 'variables', + scroll: 'scroll', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + task_manager: { + 0: '', + mapping: { + count: 'count', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .tabs-bar-container { + display: flex; + flex: inherit; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + } + .tabs-bar { + display: flex; + flex: auto; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + width: 256px; + } + .hat-btn, .bar-btn { + display: flex; + min-width: 32px; + border: none; + background: #131315; + cursor: pointer; + flex-direction: row; + justify-content: center; + align-items: center; + } + .bar-btn.active { + background: #2d4a6d; + } + ` + } + }, + 'icons/': { + '1.svg': { + $ref: 'hat.svg' + }, + '2.svg': { + $ref: 'hat.svg' + }, + '3.svg': { + $ref: 'docs.svg' + } + }, + 'actions/': { + 'command.json': { + raw: JSON.stringify([ + { + name: 'New File', + info: 'Create a new file after choosing its name and location.', + icon: 'file', + status: { + pinned: true, + default: true + }, + steps: [ + { + name: 'Enter File Name', + type: 'mandatory', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'search' } }, + { type: 'set_search_query', data: { query: 'file' } } + ] + }, + { name: 'Choose Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Open File', + info: 'Open an existing file from the selected location.', + icon: 'folder', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Select File', + type: 'mandatory', + is_completed: false, + component: 'form_input', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'default' } }, + { type: 'clear_selection', data: {} } + ] + } + ] + }, + { + name: 'Save File', + info: 'Save the current file to the chosen location and filename.', + icon: 'save', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Choose Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Enter File Name', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Settings', + info: 'Open configuration controls for the current workspace.', + icon: 'gear', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Configure Settings', + type: 'optional', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_flag', data: { flag_type: 'hubs', value: 'true' } } + ] + } + ] + }, + { + name: 'Help', + info: 'Open documentation for the current workspace.', + icon: 'help', + status: { + pinned: false, + default: false + }, + steps: [ + { name: 'View Documentation', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Terminal', + info: 'Open a terminal for the current workspace.', + icon: 'terminal', + status: { + pinned: true, + default: true + }, + steps: [ + { name: 'Open Terminal', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Search', + info: 'Search actions or workspace content using the command UI.', + icon: 'search', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Enter Search Query', + type: 'mandatory', + is_completed: false, + component: 'form_input', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'search' } }, + { type: 'set_search_query', data: { query: 'action' } } + ] + }, + { + name: 'Select Scope', + type: 'optional', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_flag', data: { flag_type: 'selection', value: 'default' } }, + { type: 'get_selected', data: {} } + ] + } + ] + }, + { + name: 'Click Rate Test', + info: 'Start a 10-click gesture. Regular clicks are documented as events; the 10th click triggers the result action.', + icon: 'timer', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Click 10 Times', + type: 'mandatory', + is_completed: false, + component: 'form_click_rate_test', + status: 'default', + data: '' + } + ] + }, + { + name: 'Split Tile', + info: 'Split the active tile in the selected direction.', + icon: 'split', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Choose Split Direction', + type: 'mandatory', + is_completed: false, + component: 'form_tile_split_choice', + status: 'default', + data: '' + } + ] + } + ]) + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/tabsbar/tabsbar.js") +},{"DOCS":6,"STATE":1,"net_helper":20,"tabs":28,"task_manager":30}],30:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = task_manager + +async function task_manager (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const docs = DOCS(__filename)(opts.sid) + const actions_file = await drive.get('actions/commands.json') + const actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + + const on_message = { + update_tab_count: handle_update_count + } + + const on = { + style: inject, + count: update_count + } + const { io, _ } = net(id) + const focus_action = { + name: 'Focus Task Manager', + info: 'Focus the task manager.', + icon: 'tasks', + status: { hidden: true }, + steps: [], + run: focus_task_manager + } + docs.register_actions(actions.concat(focus_action)) + on_task_manager_click.info = focus_action.info + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const btn = shadow.querySelector('.task-count-btn') + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + btn.onclick = docs.wrap_isolated(on_task_manager_click) + + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function on_task_manager_click (event, $) { $('Focus Task Manager') } + function focus_task_manager () { _.up('ui_focus', {}, { type: 'task_manager', sid: opts.sid }) } + + function handle_update_count (msg) { update_count(msg.data.count) } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function update_count (data) { if (btn) btn.textContent = data.toString() } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .task-count-btn { + background: #2d2d2d; + color: #fff; + border: none; + border-radius: 100%; + padding: 4px 8px; + min-width: 24px; + cursor: pointer; + display: flex; + align-items: center; + } + .task-count-btn:hover { + background: #3d3d3d; + } + ` + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'Kill Process', + info: 'Stop the selected running process after confirmation.', + icon: 'stop', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Process', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Kill', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Restart Task', + info: 'Restart the selected task after confirmation.', + icon: 'refresh', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Select Task', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Restart', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Task Details', + info: 'Open details for the selected task.', + icon: 'info', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Task', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'View Details', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'count/': { + 'value.json': { + raw: '3' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/task_manager/task_manager.js") +},{"DOCS":6,"STATE":1,"net_helper":20}],31:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') +const action_bar = require('action_bar') +const action_executor = require('action_executor') +const tabsbar = require('tabsbar') + +module.exports = taskbar + +async function taskbar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + const on_message = { + update_steps_wizard_for_app: handle_update_steps_wizard_for_app, + docs_toggle: handle_docs_toggle, + load_actions: handle_load_actions, + step_clicked: handle_step_clicked, + update_quick_actions_for_app: handle_update_quick_actions_for_app, + update_quick_actions_input: handle_update_quick_actions_input, + show_submit_btn: handle_submit_btn_toggle, + hide_submit_btn: handle_submit_btn_toggle, + add_link_tab: handle_forward_tabsbar, + remove_link_tab: handle_forward_tabsbar, + show_collapsed_tab_group: handle_forward_tabsbar, + hide_collapsed_tab_group: handle_forward_tabsbar, + tile_focus_changed: handle_forward_tabsbar, + restore_tab: handle_forward_tabsbar + } + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const action_executor_slot = shadow.querySelector('.action-executor-slot') + const action_bar_slot = shadow.querySelector('.action-bar-slot') + const tabsbar_slot = shadow.querySelector('.tabsbar-slot') + + const subs = await sdb.watch(onbatch) + io.on = { + up: io_up(), + action_bar: io_action_bar(), + action_executor: io_action_executor(), + tabsbar: io_tabsbar() + } + if (invite) io.accept(invite) + + const action_bar_el = await action_bar({ ...subs[0] }, io.invite('action_bar', { up: id })) + action_bar_el.classList.add('replaced-action-bar') + action_bar_slot.replaceWith(action_bar_el) + + const action_executor_el = await action_executor({ ...subs[1] }, io.invite('action_executor', { up: id })) + action_executor_el.classList.add('replaced-action-executor') + action_executor_slot.replaceWith(action_executor_el) + + const tabsbar_el = await tabsbar({ ...subs[2] }, io.invite('tabsbar', { up: id })) + tabsbar_el.classList.add('replaced-tabsbar') + tabsbar_slot.replaceWith(tabsbar_el) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + // --------- + // PROTOCOLS + // --------- + + function io_action_bar () { + return function action_bar_protocol (msg) { + const action_handlers = { + action_submitted: action_bar_forward_action_executor, + selected_action: action_bar_forward_action_executor, + activate_steps_wizard: action_bar_forward_action_executor, + render_form: action_bar_forward_action_executor, + console_history_toggle: action_bar_forward_up, + ui_focus: action_bar_forward_up, + display_actions: action_bar_forward_up, + filter_actions: action_bar_forward_up + } + const handler = action_handlers[msg.type] || action_bar_forward_up + handler(msg) + + function action_bar_forward_action_executor (msg) { _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function action_bar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_action_executor () { + return function action_executor_protocol (msg) { + console.error('taskbar: action_executor_protocol', msg.type, msg.data) + const action_handlers = { + load_actions: action_executor__forward_action_bar, + step_clicked: action_executor__forward_action_bar, + show_submit_btn: action_executor__forward_action_bar, + hide_submit_btn: action_executor__forward_action_bar, + action_auto_completed: action_executor__auto_completed + } + const handler = action_handlers[msg.type] || action_executor__noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function action_executor__forward_action_bar (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function action_executor__noop () {} + + function action_executor__auto_completed (msg) { _.action_bar('action_submitted', msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_tabsbar () { + return function tabsbar_protocol (msg) { + const action_handlers = { + docs_toggle: tabsbar_docs_toggle, + link_tab_close_clicked: tabsbar_forward_up, + link_tab_clicked: tabsbar_forward_up, + tab_group_tile_clicked: tabsbar_forward_up + } + const handler = action_handlers[msg.type] || tabsbar__noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function tabsbar_docs_toggle (msg) { + _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function tabsbar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function tabsbar__noop () {} + } + } + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_forward_action_bar + handler(msg) + } + } + + function handle_update_steps_wizard_for_app (msg) { _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_docs_toggle (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_load_actions (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_step_clicked (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_update_quick_actions_for_app (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_update_quick_actions_input (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_submit_btn_toggle (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_forward_tabsbar (msg) { _.tabsbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_forward_action_bar (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + action_bar: { + $: '' + }, + action_executor: { + $: '' + }, + tabsbar: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + action_bar: { + 0: '', + mapping: { + icons: 'icons', + style: 'style', + variables: 'variables', + data: 'data', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + }, + action_executor: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs', + data: 'data' + } + }, + tabsbar: { + 0: '', + mapping: { + icons: 'icons', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .taskbar-container { + display: flex; + background: #2d2d2d; + column-gap: 1px; + flex-direction: column; + align-content: center; + justify-content: center; + container-type: inline-size; + } + .replaced-tabsbar { + display: flex; + flex: auto; + } + .replaced-action-bar { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + background: #131315; + } + .replaced-action-executor { + display: flex; + } + .bottom-slot { + display: flex; + flex-direction: row; + justify-content: space-between; + } + @container (max-width: 768px) { + .bottom-slot { + flex-direction: column; + } + } + ` + } + }, + 'icons/': {}, + 'variables/': {}, + 'data/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'prefs/': {}, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/taskbar/taskbar.js") +},{"STATE":1,"action_bar":7,"action_executor":8,"net_helper":20,"tabsbar":29}],32:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const program_container = require('program_container') +const taskbar = require('taskbar') + +module.exports = theme_widget + +async function theme_widget (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + focused: handle_focused + } + + // Inline focus tracking (merged from focus_tracker) + let last_focused = null + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+ ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const program_container_slot = shadow.querySelector('.program-container-slot') + const taskbar_slot = shadow.querySelector('.taskbar-slot') + + const subs = await sdb.watch(onbatch) + + let program_container_el = null + let taskbar_el = null + io.on = { + up: io_up(), + program_container: io_program_container(), + taskbar: io_taskbar() + } + if (invite) io.accept(invite) + + taskbar_el = await taskbar({ ...subs[1] }, io.invite('taskbar', { up: id })) + taskbar_el.classList.add('taskbar') + taskbar_slot.replaceWith(taskbar_el) + + program_container_el = await program_container({ ...subs[0] }, io.invite('program_container', { up: id })) + program_container_el.classList.add('program-container') + program_container_slot.replaceWith(program_container_el) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + // Inline focus tracker: reads persisted focused value to keep last_focused in sync + function handle_focused ({ data }) { + const focused = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + last_focused = focused.value + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + + function io_up () { + return function onmessage_from_root (msg) { + const action_handlers = { + update_actions_for_app: root_update_actions_for_app, + update_quick_actions_for_app: root_forward_taskbar, + update_steps_wizard_for_app: root_forward_taskbar, + tile_focus_changed: root_forward_taskbar, + show_collapsed_tab_group: root_forward_taskbar, + hide_collapsed_tab_group: root_forward_taskbar + } + const handler = action_handlers[msg.type] || fail + handler(msg) + + function root_update_actions_for_app (msg) { + if (_.program_container) _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + else setTimeout(root_retry_send_program_container, 500, msg) + } + + function root_retry_send_program_container (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function root_forward_taskbar (msg) { _.taskbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + // Inline focus tracker: handles ui_focus messages from children + function handle_ui_focus (msg) { + if (last_focused !== msg.data.type) { + _.up('focused_app_changed', {}, msg.data) + } + drive.put('focused/current.json', { value: msg.data.type }) + } + + // --------- + // PROTOCOLS + // --------- + function io_program_container () { + return function program_container_protocol (msg) { + const action_handlers = { + ui_focus: program_container_forward_ui_focus, + set_doc_display_handler: program_container_forward_up, + action_auto_completed: program_container_forward_up, + action_complete: program_container_forward_up + } + const handler = action_handlers[msg.type] || program_container_forward_taskbar + handler(msg) + + function program_container_forward_ui_focus (msg) { handle_ui_focus(msg) } + function program_container_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function program_container_forward_taskbar (msg) { _.taskbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_taskbar () { + return function taskbar_protocol (msg) { + const action_handlers = { + ui_focus: taskbar_forward_ui_focus, + docs_toggle: taskbar_docs_toggle, + set_docs_mode: taskbar_forward_up, + action_auto_completed: taskbar_forward_up, + action_complete: taskbar_forward_up, + link_tab_close_clicked: taskbar_forward_up, + link_tab_clicked: taskbar_forward_up, + tab_group_tile_clicked: taskbar_forward_up + } + const handler = action_handlers[msg.type] || taskbar_forward_program_container + handler(msg) + + function taskbar_forward_ui_focus (msg) { handle_ui_focus(msg) } + function taskbar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function taskbar_forward_program_container (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function taskbar_docs_toggle (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + program_container: { + $: '' + }, + taskbar: { + $: '' + }, + net_helper: { + $: '' + } + }, + drive: {} + } + + function fallback_instance () { + return { + _: { + program_container: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs: 'docs', + docs_style: 'docs_style' + } + }, + taskbar: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + hardcons: 'hardcons', + docs: 'docs' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .theme-widget { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: #131315; + min-height: 0; + min-width: 400px; + } + .program-container { + flex: 1 1 auto; + min-height: 0; + height: 100%; + } + .taskbar { + flex: 0 0 auto; + width: 100%; + z-index: 10; + } + ` + } + }, + 'flags/': {}, + 'commands/': {}, + 'icons/': {}, + 'scroll/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'active_tab/': {}, + 'entries/': {}, + 'runtime/': {}, + 'mode/': {}, + 'keybinds/': {}, + 'undo/': {}, + 'focused/': { + 'current.json': { + raw: { value: 'default' } + } + }, + 'temp_actions/': {}, + 'temp_quick_actions/': {}, + 'prefs/': {}, + 'variables/': {}, + 'data/': {}, + 'docs_style/': {}, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/theme_widget/theme_widget.js") +},{"STATE":1,"net_helper":20,"program_container":22,"taskbar":31}],33:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const theme_widget = require('theme_widget') +const tab_group = require('tab_group') + +module.exports = tile_manager + +async function tile_manager(opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const layout = { + root: { type: 'leaf', tile_id: 0, el: null }, + collapse_level: 0, + manual_level: null, + focused_tile: 0 + } + const tile_registry = {} + + const cached_actions = { + update_actions_for_app: null, + update_quick_actions_for_app: null, + update_steps_wizard_for_app: null + } + + const tile_tabs = {} + + let tile_counter = 1 + let split_counter = 1 + let last_width = 0 + let last_height = 0 + const MIN_TILE_WIDTH = 300 + const MIN_TILE_HEIGHT = 200 + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = `
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const wrapper = shadow.querySelector('.tile-manager-wrapper') + const container = shadow.querySelector('.tile-manager') + + const subs = await sdb.watch(onbatch) + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const resize_observer = new ResizeObserver(on_resize_observed) + resize_observer.observe(el) + + container.addEventListener('pointerdown', function (e) { + const slot = e.target.closest ? e.target.closest('.tile-slot') : null + if (!slot) return + const tile_id = parseInt(slot.getAttribute('data-tile-id'), 10) + if (!isNaN(tile_id) && tile_id !== layout.focused_tile) { + set_focused_tile(tile_id) + } + }, true) + + function on_resize_observed(entries) { + for (const entry of entries) { + handle_resize(entry.contentRect.width, entry.contentRect.height) + } + } + + await render_layout_tree() + + return el + + function get_tree_depth(node) { + if (!node) return 0 + if (node.type === 'leaf') return 0 + let max_child_depth = 0 + for (const child of node.children) { + const d = get_tree_depth(child) + if (d > max_child_depth) max_child_depth = d + } + return 1 + max_child_depth + } + + function get_max_collapse_level() { + return get_tree_depth(layout.root) + } + + function pick_representative_child(node) { + return node.children.find(has_tile_0) || node.children[0] + } + + function has_tile_0(node) { + if (!node) return false + if (node.type === 'leaf') return node.tile_id === 0 + return node.children.some(child => has_tile_0(child)) + } + + function resolve_representative_leaf(node) { + if (node.type === 'leaf') return node.tile_id + return resolve_representative_leaf(pick_representative_child(node)) + } + + function measure_min_size(node, depth, prune_depth) { + if (!node) return { width: 0, height: 0 } + if (node.type === 'leaf') return { width: MIN_TILE_WIDTH, height: MIN_TILE_HEIGHT } + if (depth >= prune_depth) return measure_min_size(pick_representative_child(node), depth + 1, prune_depth) + + const sizes = node.children.map(measure_child) + const widths = sizes.map(size => size.width) + const heights = sizes.map(size => size.height) + if (node.direction === 'horizontal') return { width: sum(widths), height: Math.max(...heights) } + return { width: Math.max(...widths), height: sum(heights) } + + function measure_child(child) { return measure_min_size(child, depth + 1, prune_depth) } + } + + function sum(values) { return values.reduce(add_value, 0) } + + function add_value(total, value) { return total + value } + + function pick_collapse_level(width, height) { + const max_level = get_max_collapse_level() + for (let level = 0; level < max_level; level++) { + const size = measure_min_size(layout.root, 0, max_level - level) + if (size.width <= width && size.height <= height) return level + } + return max_level + } + + function pick_level_showing_tile(tile_id) { + const max_level = get_max_collapse_level() + for (let level = max_level; level > 0; level--) { + if (collect_leaf_ids(layout.root, max_level - level).includes(tile_id)) return level + } + return 0 + } + + function handle_resize(width, height) { + if (!width || !height) return + if (width === last_width && height === last_height) return + last_width = width + last_height = height + layout.manual_level = null + refresh_layout().catch(on_layout_error) + } + + function set_collapse_level(level) { + const max_level = get_max_collapse_level() + layout.manual_level = Math.max(0, Math.min(level, max_level)) + console.error('tile_manager: set_collapse_level', layout.manual_level, '(max:', max_level, ')') + refresh_layout().catch(on_layout_error) + } + + function refresh_layout() { + const responsive_level = last_width ? pick_collapse_level(last_width, last_height) : layout.collapse_level + layout.collapse_level = layout.manual_level === null ? responsive_level : layout.manual_level + return render_layout_tree() + } + + function on_layout_error(err) { console.error('tile_manager: layout render failed', err) } + + function get_tiles() { + return collect_leaf_ids().map(tile_id => tile_registry[tile_id]).filter(Boolean) + } + + function collect_leaf_ids(node = layout.root, prune_depth = Infinity, depth = 0, tile_ids = []) { + if (!node) return tile_ids + if (node.type === 'leaf') { + tile_ids.push(node.tile_id) + return tile_ids + } + if (depth >= prune_depth) { + return collect_leaf_ids(pick_representative_child(node), prune_depth, depth + 1, tile_ids) + } + node.children.forEach(child => collect_leaf_ids(child, prune_depth, depth + 1, tile_ids)) + return tile_ids + } + + function group_hidden_groups_by_anchor(node, depth, prune_depth, groups_by_tile = new Map()) { + if (!node || node.type === 'leaf') return groups_by_tile + if (depth < prune_depth) { + node.children.forEach(child => group_hidden_groups_by_anchor(child, depth + 1, prune_depth, groups_by_tile)) + return groups_by_tile + } + + const kept_child = pick_representative_child(node) + const hidden_children = node.children.filter(child => child !== kept_child) + if (hidden_children.length) { + const anchor_tile_id = resolve_representative_leaf(kept_child) + const group = { + type: 'collapsed_group', + split_id: node.split_id, + direction: node.direction, + depth: depth, + children: hidden_children.map(describe_subtree) + } + const groups = groups_by_tile.get(anchor_tile_id) || [] + groups.push(group) + groups_by_tile.set(anchor_tile_id, groups) + } + return group_hidden_groups_by_anchor(kept_child, depth + 1, prune_depth, groups_by_tile) + } + + function describe_subtree(node) { + if (!node) return null + if (node.type === 'leaf') { + return { + type: 'leaf', + tile_id: node.tile_id, + tabs: tile_tabs[node.tile_id] || [], + children: [] + } + } + return { + type: 'split_group', + split_id: node.split_id, + direction: node.direction, + children: node.children.map(describe_subtree).filter(Boolean) + } + } + + function find_leaf_node(tile_id, node = layout.root, parent = null, index = -1) { + if (!node) return null + if (node.type === 'leaf') { + return node.tile_id === tile_id ? { node, parent, index } : null + } + for (let child_index = 0; child_index < node.children.length; child_index++) { + const match = find_leaf_node(tile_id, node.children[child_index], node, child_index) + if (match) return match + } + return null + } + + function replace_leaf_node(tile_id, next_node) { + const match = find_leaf_node(tile_id) + if (!match) return false + if (!match.parent) { + layout.root = next_node + return true + } + match.parent.children[match.index] = next_node + return true + } + + async function ensure_tile(tile_id) { + if (tile_registry[tile_id]) return tile_id + + const sub_entry = subs[tile_id] || { sid: opts.sid } + const tile_info = { + id: tile_id, + element: null, + slot: null, + sid: sub_entry.sid + } + tile_registry[tile_id] = tile_info + console.error('tile_manager: tile_info added for tile', tile_id) + + io.on[`tile_${tile_id}`] = io_tile(tile_id) + console.error('tile_manager: handler registered for tile_' + tile_id) + const tile_invite = io.invite(`tile_${tile_id}`, { up: id }) + console.error('tile_manager: invite created for tile_' + tile_id, '_[tile_' + tile_id + '] exists:', !!_[`tile_${tile_id}`]) + + const component = tile_id === 0 ? theme_widget : tab_group + const tile_el = await component( + { ...sub_entry, ids: { up: id } }, + tile_invite + ) + + tile_info.element = tile_el + + console.error('tile_manager: created tile', tile_id) + return tile_id + } + + async function render_layout_tree() { + const max_level = get_max_collapse_level() + const collapse_level = Math.max(0, Math.min(layout.collapse_level, max_level)) + const prune_depth = max_level - collapse_level + + layout.collapse_level = collapse_level + wrapper.setAttribute('data-collapse-level', collapse_level) + wrapper.setAttribute('data-max-level', max_level) + + const rendered_tree = await render_node(layout.root, 0, prune_depth) + sync_dom_children(container, rendered_tree ? [rendered_tree] : []) + + sync_collapsed_tab_strip(collapse_level, max_level, prune_depth) + set_focused_tile(pick_visible_focus(prune_depth)) + } + + async function render_node(node, depth, prune_depth) { + if (!node) return null + if (node.type === 'leaf') return render_leaf_node(node.tile_id) + if (depth >= prune_depth) return render_node(pick_representative_child(node), depth + 1, prune_depth) + + if (!node.el) node.el = document.createElement('div') + node.el.className = `tile-split ${node.direction}` + + const child_elements = [] + for (const child of node.children) { + const child_el = await render_node(child, depth + 1, prune_depth) + if (child_el) child_elements.push(child_el) + } + sync_dom_children(node.el, child_elements) + return node.el + } + + function sync_collapsed_tab_strip(collapse_level, max_level, prune_depth) { + const groups_by_tile = collapse_level > 0 ? group_hidden_groups_by_anchor(layout.root, 0, prune_depth) : new Map() + + for (const tile_id of collect_leaf_ids(layout.root, prune_depth)) { + const send = _[`tile_${tile_id}`] + if (!send) continue + + const collapsed_groups = groups_by_tile.get(tile_id) + if (!collapsed_groups) { + send('hide_collapsed_tab_group', {}, {}) + continue + } + + send('show_collapsed_tab_group', {}, { + nested: true, + collapse_level: collapse_level, + max_level: max_level, + collapsed_groups: collapsed_groups + }) + } + } + + function pick_visible_focus(prune_depth) { + const visible_ids = collect_leaf_ids(layout.root, prune_depth) + if (visible_ids.includes(layout.focused_tile)) return layout.focused_tile + return visible_ids.length ? visible_ids[0] : layout.focused_tile + } + + async function render_leaf_node(tile_id) { + const match = find_leaf_node(tile_id) + const node = match?.node + if (!node) return null + await ensure_tile(tile_id) + const tile_info = tile_registry[tile_id] + if (!tile_info) return null + + if (!node.el) { + node.el = document.createElement('div') + node.el.className = 'tile-slot' + } + node.el.setAttribute('data-tile-id', tile_id) + tile_info.slot = node.el + + if (node.el.firstChild !== tile_info.element) { + node.el.replaceChildren(tile_info.element) + } + return node.el + } + + function sync_dom_children(parent, desired_children) { + let current_index = 0 + + for (const child of desired_children) { + const current_child = parent.childNodes[current_index] + if (current_child !== child) { + parent.insertBefore(child, current_child || null) + } + current_index++ + } + + while (parent.childNodes.length > desired_children.length) { + parent.removeChild(parent.lastChild) + } + } + + function io_tile(tile_id) { + return function tile_protocol(msg) { + const { type, data } = msg + console.error(`tile_manager: message from tile_${tile_id}`, type, data) + + if (type === 'tab_group_tile_clicked') { + const target_tile = data && typeof data.tile_id === 'number' ? data.tile_id : null + console.error('tile_manager: folded tab clicked, revealing tile', target_tile) + if (target_tile !== null) layout.focused_tile = target_tile + set_collapse_level(target_tile === null ? 0 : pick_level_showing_tile(target_tile)) + return + } + + if (tile_id === 0 && type === 'request_collapse_level') { + if (data && typeof data.level === 'number') { + set_collapse_level(data.level) + } + return + } + + if (type === 'ui_focus') { + set_focused_tile(tile_id) + } + + if (type === 'tab_name_clicked' && tile_id !== 0) { + if (data && data.id && tile_tabs[tile_id]) { + const exists = tile_tabs[tile_id].find(t => t.id === data.id) + if (!exists) { + tile_tabs[tile_id].push({ id: data.id, name: data.name, program: data.program }) + } + } + } + if (type === 'tab_close_clicked' && tile_id !== 0) { + if (data && data.id && tile_tabs[tile_id]) { + tile_tabs[tile_id] = tile_tabs[tile_id].filter(t => t.id !== data.id) + } + } + + if (tile_id !== 0 && type === 'all_tabs_closed') { + console.error('tile_manager: all tabs closed in split tile', tile_id) + handle_merge(tile_id) + return + } + + if (type === 'action_auto_completed' || type === 'action_complete') { + console.error('tile_manager: action completed, checking for split', data) + const action = data?.selected_action + console.error('tile_manager: action name:', action?.name) + if (action?.name === 'Split Tile') { + let direction = null + + if (data.result) { + try { + const results = JSON.parse(data.result) + direction = results[0] + console.error('tile_manager: direction from result:', direction) + } catch (e) { + console.error('tile_manager: failed to parse result', e) + } + } + + if (!direction) { + const split_step = action.steps.find(s => s.component === 'form_tile_split_choice') + console.error('tile_manager: split_step found:', split_step) + if (split_step && split_step.data) { + direction = split_step.data + } + } + + if (direction) { + console.error('tile_manager: split requested', direction, 'from tile', tile_id) + handle_split(tile_id, direction) + } else { + console.error('tile_manager: no direction found, cannot split') + } + } + } + + const refs = msg.head ? { cause: msg.head } : {} + _.up?.(type, refs, data) + } + } + + async function handle_split(source_tile_id, direction) { + const source_match = find_leaf_node(source_tile_id) + if (!source_match) { + console.error('tile_manager: source tile not found for split', source_tile_id) + return + } + + const new_tile_id = tile_counter++ + const new_leaf = { type: 'leaf', tile_id: new_tile_id, el: null } + const source_leaf = source_match.node + const is_horizontal = direction === 'left' || direction === 'right' + const split_node = { + type: 'split', + split_id: split_counter++, + direction: is_horizontal ? 'horizontal' : 'vertical', + split_direction: direction, + children: direction === 'left' || direction === 'up' + ? [new_leaf, source_leaf] + : [source_leaf, new_leaf], + el: null + } + + replace_leaf_node(source_tile_id, split_node) + + await ensure_tile(new_tile_id) + send_cached_actions_to_tile(new_tile_id) + + const new_tile_send = _[`tile_${new_tile_id}`] + if (new_tile_send) { + const tab_data = { name: 'New Tab', program: 'text_editor' } + new_tile_send('create_default_tab', {}, tab_data) + if (!tile_tabs[new_tile_id]) tile_tabs[new_tile_id] = [] + tile_tabs[new_tile_id].push({ id: `tab_initial_${new_tile_id}`, name: tab_data.name, program: tab_data.program }) + } + + await refresh_layout() + + console.error('tile_manager: split complete', direction, get_tiles().length, 'tiles') + } + + function handle_merge(tile_id_to_remove) { + console.error('tile_manager: handle_merge', tile_id_to_remove) + + const match = find_leaf_node(tile_id_to_remove) + if (!match || !match.parent) { + console.error('tile_manager: tile not found for merge', tile_id_to_remove) + return + } + + const sibling_index = match.index === 0 ? 1 : 0 + const sibling_node = match.parent.children[sibling_index] + + if (match.parent === layout.root) { + layout.root = sibling_node + } else { + const did_replace = replace_split_parent(layout.root, match.parent, sibling_node) + if (!did_replace) { + console.error('tile_manager: failed to replace merge parent for tile', tile_id_to_remove) + return + } + } + + const tile_info = tile_registry[tile_id_to_remove] + if (tile_info?.element?.parentNode) { + tile_info.element.parentNode.remove() + } + delete tile_registry[tile_id_to_remove] + delete tile_tabs[tile_id_to_remove] + + refresh_layout().catch(on_layout_error) + + console.error('tile_manager: merge complete, tiles remaining:', get_tiles().length) + } + + function replace_split_parent(node, target_parent, replacement_node) { + if (!node || node.type === 'leaf') return false + for (let index = 0; index < node.children.length; index++) { + if (node.children[index] === target_parent) { + node.children[index] = replacement_node + return true + } + if (replace_split_parent(node.children[index], target_parent, replacement_node)) return true + } + return false + } + + function set_focused_tile(tile_id) { + console.error('tile_manager: set_focused_tile', tile_id) + layout.focused_tile = tile_id + + for (const tile of get_tiles()) { + const slot = tile.slot + if (slot) { + slot.classList.toggle('focused', tile.id === tile_id) + } + } + + for (const tile of get_tiles()) { + const send = _[`tile_${tile.id}`] + if (send) { + send('tile_focus_changed', {}, { focused_tile: tile_id, is_focused: tile.id === tile_id }) + } + } + } + + function send_cached_actions_to_tile(tile_id) { + const send = _[`tile_${tile_id}`] + if (!send) { + console.error('tile_manager: cannot send cached actions, tile_' + tile_id + ' not found') + return + } + + console.error('tile_manager: sending cached actions to tile_' + tile_id) + + for (const [type, data] of Object.entries(cached_actions)) { + if (data !== null) { + console.error('tile_manager: sending cached', type, 'to tile_' + tile_id) + send(type, {}, data) + } + } + } + + function io_up() { + const cacheable = { + update_actions_for_app: true, + update_quick_actions_for_app: true, + update_steps_wizard_for_app: true + } + return function onmessage(msg) { + const { type, data } = msg + console.error('tile_manager: message from root', type, 'tiles:', get_tiles().length) + if (cacheable[type]) { + cached_actions[type] = data + console.error('tile_manager: cached', type) + } + const refs = msg.head ? { cause: msg.head } : {} + for (const tile of get_tiles()) { + const send = _[`tile_${tile.id}`] + if (send) send(type, refs, data) + } + } + } + + async function onbatch(batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw(path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw(file) { return file.raw } + } + + function inject({ data }) { sheet.replaceSync(data[0]) } + + function fail({ data, type }) { console.warn('tile_manager: invalid message', { cause: { data, type } }) } +} + +function fallback_module() { + return { + api: fallback_instance, + _: { + theme_widget: { + $: '' + }, + tab_group: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance() { + return { + _: { + theme_widget: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs: 'docs', + docs_style: 'docs_style' + } + }, + tab_group: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + 6: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + docs_style: 'docs_style' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'tile_manager.css': { + $ref: 'style/tile_manager.css' + } + } + } + } + } +} + +}).call(this)}).call(this,"/src/node_modules/tile_manager/tile_manager.js") +},{"STATE":1,"net_helper":20,"tab_group":26,"theme_widget":32}],34:[function(require,module,exports){ +(function (__filename){(function (){ +const STATE = require('STATE') +const statedb = STATE(__filename) +const admin_api = statedb.admin() +const admin_on = {} +admin_api.on(handle_admin_message) +const { sdb, io: sdbio, id } = statedb(fallback_module) +const { drive, admin } = sdb +const net = require('net_helper') +const DOCS = require('DOCS') +const docs = DOCS(__filename)() +const docs_admin = docs.admin +/****************************************************************************** + PAGE +******************************************************************************/ +const navbar = require('menu') +const theme_widget = require('theme_widget') +const taskbar = require('taskbar') +const tabsbar = require('tabsbar') +const action_bar = require('action_bar') +const program_container = require('program_container') +const tabs = require('tabs') +const console_history = require('console_history') +const tile_manager = require('tile_manager') +const actions = require('actions') +const tabbed_editor = require('tabbed_editor') +const task_manager = require('task_manager') +const quick_actions = require('quick_actions') +const graph_viewer = require('graph_viewer') +const editor = require('quick_editor') +const action_executor = require('action_executor') +const steps_wizard = require('steps_wizard') +const { resource } = require('helpers') + +const imports = { + tile_manager, + theme_widget, + taskbar, + tabsbar, + action_bar, + program_container, + tabs, + console_history, + actions, + tabbed_editor, + task_manager, + quick_actions, + graph_viewer, + action_executor, + steps_wizard +} +module.exports = ui_gallery + +/****************************************************************************** + PAGE BOOT +******************************************************************************/ +async function ui_gallery (opts = {}) { + // ---------------------------------------- + // ID + JSON STATE + // ---------------------------------------- + let resize_enabled = true + const on = { + style: inject, + resize_container: update_resize, + ...sdb.admin.status.dataset.drive, + ...sdb.admin + } + // const status = {} + // ---------------------------------------- + // TEMPLATE + // ---------------------------------------- + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` + +
+
+
` + document.body.style.margin = 0 + document.body.style.backgroundColor = '#d8dee9' + + // ---------------------------------------- + // ELEMENTS + // ---------------------------------------- + + const navbar_slot = shadow.querySelector('.navbar-slot') + const components_wrapper = shadow.querySelector('.components-wrapper') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const entries = Object.entries(imports) + const wrappers = [] + const names = entries.map(get_entry_name) + let current_selected_wrapper = null + + function get_entry_name (entry) { return entry[0] } + + const url_params = new URLSearchParams(window.location.search) + const checked_param = url_params.get('checked') + const selected_name_param = url_params.get('selected') + let initial_checked_indices = [] + + if (checked_param) { + try { + const parsed = JSON.parse(checked_param) + if (Array.isArray(parsed) && parsed.every(Number.isInteger)) { + initial_checked_indices = parsed + } else { + console.warn('Invalid "checked" URL parameter format.') + } + } catch (e) { + console.error('Error parsing "checked" URL parameter:', e) + } + } + + const menu_callbacks = { + on_checkbox_change: handle_checkbox_change, + on_label_click: handle_label_click, + on_select_all_toggle: handle_select_all_toggle, + on_resize_toggle: handle_resize_toggle + } + const item = resource() + sdbio.on(register_io_port) + const { io, _: send } = net(id) + io.on = { + theme_widget: io_theme_widget(), + up: io_up() + } + const preview_names = Object.keys(imports) + for (const name of preview_names) { + if (name === 'theme_widget') continue + io.on[name] = io_noop() + } + + function register_io_port (port) { + const { by, to } = port + item.set(port.to, port) + + port.onmessage = on_port_message + + function on_port_message (event) { + const txt = event.data + const key = `[${by} -> ${to}]` + console.log('[ port-stuff ]', key) + + on[txt.type](...txt.data) + } + } + + const editor_subs = await sdb.get_sub('ui_gallery>quick_editor') + // const subs = await sdb.watch(onbatch) + const subs = (await sdb.watch(onbatch)).filter(is_even_index) + + function is_even_index (_, index) { return index % 2 === 0 } + + console.log('Page subs', subs) + const nav_menu_element = await navbar(subs[names.length], names, initial_checked_indices, menu_callbacks) + + const main_editor = editor_subs[0] ? await editor(editor_subs[0]) : null + navbar_slot.replaceWith(nav_menu_element, main_editor || document.createElement('div')) + await create_component(entries) + update_resize(resize_enabled) + window.onload = scroll_to_initial_selected + send_quick_editor_data() + admin_on.import = send_quick_editor_data + + function io_theme_widget () { + return function theme_widget_protocol (msg) { + const action_handlers = { + set_docs_mode: handle_set_docs_mode, + set_doc_display_handler: handle_set_doc_display_handler, + focused_app_changed: handle_focused_app_changed + } + const handler = action_handlers[msg.type] || handle_fail + handler(msg) + + function handle_set_docs_mode (msg) { docs_admin.set_docs_mode(msg.data.active) } + function handle_set_doc_display_handler (msg) { docs_admin.set_doc_display_handler(msg.data.callback) } + function handle_fail (msg) { console.warn('page: unhandled message from theme_widget', msg) } + + function handle_focused_app_changed (msg) { + const actions = docs_admin.get_actions(msg.data.sid) + update_actions_for_app(actions, msg) + } + + async function update_actions_for_app (data, msg) { + const refs = msg.head ? { cause: msg.head } : {} + send.theme_widget('update_actions_for_app', refs, data) + send.theme_widget('update_quick_actions_for_app', refs, data) + send.theme_widget('update_steps_wizard_for_app', refs, data) + } + } + } + + function io_up () { + return function () {} + } + + function io_noop () { + return function () {} + } + return el + async function create_component (entries_obj) { + let index = 0 + const component_counters = {} + + for (const [name, factory] of entries_obj) { + const is_initially_checked = initial_checked_indices.length === 0 || initial_checked_indices.includes(index + 1) + const outer = document.createElement('div') + outer.className = 'component-outer-wrapper' + outer.style.display = is_initially_checked ? 'block' : 'none' + outer.innerHTML = ` +
${name}
+
+ ` + const inner = outer.querySelector('.component-wrapper') + let component_content + + // Match sub to factory by component name in type field + component_counters[name] = (component_counters[name] || 0) + 1 + const matching_subs = subs.filter(s => s.type && s.type.endsWith(`>${name}`)) + const occurrence_index = component_counters[name] - 1 + const sub = matching_subs[occurrence_index] + + if (!sub) { + console.error(`No sub found for component: ${name} \n make sure that the imports variable property name is same as the required component name`) + index++ + continue + } + + if (name === 'theme_widget' || name === 'tile_manager') { + component_content = await factory({ ...sub }, io.invite('theme_widget', { up: id })) + } else { + component_content = await factory({ ...sub }, io.invite(name, { up: id })) + } + component_content.className = 'component-content' + + const node_id = admin.status.s2i[sub.sid] + const editor_index = index + 1 + const component_editor = editor_subs[editor_index] ? await editor(editor_subs[editor_index]) : null + inner.append(component_content, component_editor || document.createElement('div')) + + const result = {} + const drive = admin.status.dataset.drive + + const modulepath = node_id.split(':')[0] + const fields = admin.status.db.read_all(['state', modulepath]) + const nodes = Object.keys(fields).filter(is_state_node) + + function is_state_node (field) { return !isNaN(Number(field.split(':').at(-1))) } + + for (const node of nodes) { + result[node] = {} + const datasets = drive.list('', node) + for (const dataset of datasets) { + result[node][dataset] = {} + const files = drive.list(dataset, node) + for (const file of files) { + result[node][dataset][file] = (await drive.get(dataset + file, node)).raw + } + } + } + + if (editor_subs[editor_index]) { + const editor_id = admin.status.a2i[admin.status.s2i[editor_subs[editor_index].sid]] + const port = await item.get(editor_id) + // await sdbio.at(editor_id) + port.postMessage(result) + } + + components_wrapper.appendChild(outer) + wrappers[index] = { outer, inner, name, checkbox_state: is_initially_checked } + index++ + } + } + + function scroll_to_initial_selected () { + if (selected_name_param) { + const index = names.indexOf(selected_name_param) + if (index !== -1 && wrappers[index]) { + const target_wrapper = wrappers[index].outer + if (target_wrapper.style.display !== 'none') { + setTimeout(scroll_to_selected_wrapper, 100) + + function scroll_to_selected_wrapper () { + target_wrapper.scrollIntoView({ behavior: 'auto', block: 'center' }) + clear_selection_highlight() + target_wrapper.style.backgroundColor = '#2e3440' + current_selected_wrapper = target_wrapper + } + } + } + } + } + + function clear_selection_highlight () { + if (current_selected_wrapper) { + current_selected_wrapper.style.backgroundColor = '' + } + current_selected_wrapper = null + } + + function update_url (selected_name = url_params.get('selected')) { + const checked_indices = wrappers.reduce(collect_checked_index, []) + + function collect_checked_index (acc, wrapper_entry, index) { + if (wrapper_entry.checkbox_state) { acc.push(index + 1) } + return acc + } + + const params = new URLSearchParams() + if (checked_indices.length > 0 && checked_indices.length < wrappers.length) { + params.set('checked', JSON.stringify(checked_indices)) + } + const selected_index = names.indexOf(selected_name) + if (selected_name && selected_index !== -1 && wrappers[selected_index].checkbox_state) { + params.set('selected', selected_name) + } + const new_url = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}` + window.history.replaceState(null, '', new_url) + } + + function handle_checkbox_change (detail) { + const { index, checked } = detail + if (wrappers[index]) { + wrappers[index].outer.style.display = checked ? 'block' : 'none' + wrappers[index].checkbox_state = checked + update_url() + if (!checked && current_selected_wrapper === wrappers[index].outer) { + clear_selection_highlight() + update_url(null) + } + } + } + + function handle_label_click (detail) { + const { index, name } = detail + if (wrappers[index]) { + const target_wrapper = wrappers[index].outer + if (target_wrapper.style.display === 'none') { + target_wrapper.style.display = 'block' + wrappers[index].checkbox_state = true + } + target_wrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }) + clear_selection_highlight() + target_wrapper.style.backgroundColor = 'lightblue' + current_selected_wrapper = target_wrapper + update_url(name) + } + } + + function handle_select_all_toggle (detail) { + const { selectAll: select_all } = detail + wrappers.forEach(update_wrapper_visibility) + + function update_wrapper_visibility (wrapper_entry) { + wrapper_entry.outer.style.display = select_all ? 'block' : 'none' + wrapper_entry.checkbox_state = select_all + } + + clear_selection_highlight() + update_url(null) + } + + function handle_resize_toggle () { + console.log('handle_resize_toggle', resize_enabled) + resize_enabled = !resize_enabled + drive.put('resize_container/state.json', resize_enabled) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn(__filename + 'invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function update_resize (data) { + console.log('[ update_resize ]', data) + resize_enabled = data + wrappers.forEach(update_wrapper_resize) + + function update_wrapper_resize (wrap) { + const wrapper = wrap.outer.querySelector('.component-wrapper') + if (wrapper) { + wrapper.style.resize = resize_enabled ? 'both' : 'none' + wrapper.style.overflow = resize_enabled ? 'hidden' : 'visible' + } + } + } + async function send_quick_editor_data () { + const roots = admin.status.db.read(['root_datasets']) + const result = {} + roots.forEach(add_root_dataset) + + function add_root_dataset (root_dataset) { + const root = root_dataset.name + result[root] = {} + const inputs = sdb.admin.get_dataset({ root }) || [] + inputs.forEach(add_input_type) + + function add_input_type (type) { + result[root][type] = {} + const datasets = sdb.admin.get_dataset({ root, type }) + + if (!datasets) return + Object.values(datasets).forEach(add_dataset_name) + + function add_dataset_name (dataset_name) { + result[root][type][dataset_name] = {} + const dataset_ids = sdb.admin.get_dataset({ root, type, name: dataset_name }) + dataset_ids.forEach(add_dataset_id) + + function add_dataset_id (dataset_id) { + const files = admin.status.db.read([root, dataset_id]).files || [] + result[root][type][dataset_name][dataset_id] = {} + files.forEach(add_file_data) + + function add_file_data (file_id) { result[root][type][dataset_name][dataset_id][file_id] = admin.status.db.read([root, file_id]) } + } + } + } + } + + if (!editor_subs[0]) return + const editor_id = admin.status.a2i[admin.status.s2i[editor_subs[0].sid]] + const port = await item.get(editor_id) + // await sdbio.at(editor_id) + port.postMessage(result) + } +} +function fallback_module () { + const menuname = 'menu' + const names = [ + 'tile_manager', + 'theme_widget', + 'taskbar', + 'tabsbar', + 'action_bar', + 'program_container', + 'tabs', + 'console_history', + 'actions', + 'tabbed_editor', + 'task_manager', + 'quick_actions', + 'graph_viewer', + 'action_executor', + 'steps_wizard' + ] + const subs = {} + names.forEach(subgen) + subs.helpers = 0 + subs.DOCS = 0 + subs.net_helper = 0 + subs.taskbar = { + $: '', + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + hardcons: 'hardcons', + docs: 'docs' + } + } + subs.tabs = { + $: '', + 0: '', + mapping: { + icons: 'icons', + variables: 'variables', + scroll: 'scroll', + style: 'style', + docs: 'docs', + actions: 'actions' + } + } + subs.program_container = { + $: '', + 0: '', + mapping: { + style: 'style', + flags: 'flags', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + keybinds: 'keybinds', + undo: 'undo', + docs_style: 'docs_style', + docs: 'docs' + } + } + subs.action_executor = { + $: '', + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs', + data: 'data' + } + } + subs.steps_wizard = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.tabsbar = { + $: '', + 0: '', + mapping: { + icons: 'icons', + style: 'style', + docs: 'docs', + actions: 'actions' + } + } + subs.action_bar = { + $: '', + 0: '', + mapping: { + icons: 'icons', + style: 'style', + actions: 'actions', + variables: 'variables', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + } + subs.console_history = { + $: '', + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + docs: 'docs', + actions: 'actions' + } + } + subs.actions = { + $: '', + 0: '', + mapping: { + actions: 'actions', + icons: 'icons', + hardcons: 'hardcons', + style: 'style', + docs: 'docs' + } + } + subs.tabbed_editor = { + $: '', + 0: '', + mapping: { + style: 'style', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + docs: 'docs' + } + } + subs.task_manager = { + $: '', + 0: '', + mapping: { + style: 'style', + count: 'count', + docs: 'docs', + actions: 'actions' + } + } + subs.quick_actions = { + $: '', + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + } + subs[menuname] = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.quick_editor = { + $: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.theme_widget = { + $: '', + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs_style: 'docs_style', + docs: 'docs' + } + } + subs.graph_viewer = { + $: '', + 0: '', + mapping: { + theme: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + } + for (let i = 0; i < Object.keys(subs).length - 1; i++) { + subs.quick_editor[i] = quick_editor$ + } + + return { + _: subs, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .components-wrapper-container { + padding-top: 10px; /* Adjust as needed */ + } + + .component-outer-wrapper { + margin-bottom: 20px; + padding: 0px 0px 10px 0px; + transition: background-color 0.3s ease; + } + + .component-name-label { + background-color:transparent; + padding: 8px 15px; + text-align: center; + font-weight: bold; + color: #333; + } + + .component-wrapper { + width: 95%; + margin: 0 auto; + position: relative; + padding: 15px; + border: 3px solid #666; + resize: none; + overflow: visible; + border-radius: 0px; + background-color: #eceff4; + min-height: 50px; + } + .component-content { + width: 100%; + height: 100%; + } + .toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; + } + + .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: #ccc; + border-radius: 26px; + transition: 0.4s; + } + + .slider::before { + content: ""; + position: absolute; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: 0.4s; + } + + input:checked + .slider { + background-color: #2196F3; + } + + input:checked + .slider::before { + transform: translateX(24px); + } + .component-wrapper:hover::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + border: 4px solid skyblue; + pointer-events: none; + z-index: 15; + resize: both; + overflow: hidden; + } + .quick-editor { + position: absolute; + z-index: 100; + top: 0; + right: 0; + } + .component-wrapper:hover .quick-editor { + display: block; + } + .component-wrapper > .quick-editor { + display: none; + top: -5px; + right: -10px; + }` + } + }, + 'resize_container/': { + 'state.json': { + raw: 'false' + } + }, + 'icons/': {}, + 'variables/': {}, + 'scroll/': {}, + 'commands/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'count/': {}, + 'entries/': {}, + 'active_tab/': {}, + 'runtime/': {}, + 'mode/': {}, + 'data/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'undo/': {}, + 'focused/': {}, + 'temp_actions/': {}, + 'temp_quick_actions/': {}, + 'prefs/': {}, + 'docs_style/': {}, + 'docs/': {} + } + } + function quick_editor$ (args, tools, [quick_editor]) { + const state = quick_editor() + state.net = { + page: {} + } + return state + } + function subgen (name) { + subs[name] = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + } +} + +function handle_admin_message (msg) { + const { type } = msg + admin_on[type] && admin_on[type]() +} + +}).call(this)}).call(this,"/src/node_modules/ui_gallery/index.js") +},{"DOCS":6,"STATE":1,"action_bar":7,"action_executor":8,"actions":9,"console_history":10,"graph_viewer":15,"helpers":17,"menu":19,"net_helper":20,"program_container":22,"quick_actions":23,"quick_editor":24,"steps_wizard":25,"tabbed_editor":27,"tabs":28,"tabsbar":29,"task_manager":30,"taskbar":31,"theme_widget":32,"tile_manager":33}],35:[function(require,module,exports){ +const ui_gallery = require('../src/index') +config().then(boot_default_page) + +async function config () { + const html = document.documentElement + const meta = document.createElement('meta') + const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const loadFont = `` + html.setAttribute('lang', 'en') + meta.setAttribute('name', 'viewport') + meta.setAttribute('content', 'width=device-width,initial-scale=1.0') + document.head.append(meta) + document.head.insertAdjacentHTML('beforeend', loadFont) + await document.fonts.ready +} + +async function boot_default_page () { + document.body.append(await ui_gallery()) +} + +},{"../src/index":4}]},{},[35]); diff --git a/guide/README.md b/guide/README.md new file mode 100644 index 0000000..6e96264 --- /dev/null +++ b/guide/README.md @@ -0,0 +1,31 @@ +# UI Components Guide + +Use this guide when you are building, reusing, or modifying components in this repository. + +## Start here + +- New to the repo: read [cheat-sheet.md](./cheat-sheet.md). +- Need strict code rules: read [coding-standards.md](./coding-standards.md). +- Creating a component: read [create-component.md](./create-component.md). +- Using an existing component in another app: read [use-existing-component.md](./use-existing-component.md). +- Building a browser preview or demo page: read [demo-pages.md](./demo-pages.md). +- Working on theme widget data or styling: read [theme-widget.md](./theme-widget.md). +- Working on actions and documentation: read [actions-and-docs.md](./actions-and-docs.md). + +## Data Shell + +Data Shell docs explain STATE, datasets, mappings, and component messaging. + +- Start with [datashell/README.md](./datashell/README.md). +- For persistent component data: read [datashell/state.md](./datashell/state.md). +- For parent/child communication: read [datashell/protocol.md](./datashell/protocol.md). +- For the exact router API: read [datashell/net-helper.md](./datashell/net-helper.md). + +## Examples + +Complete examples live in [examples/](./examples/). + +- [examples/component.js](./examples/component.js) +- [examples/page.js](./examples/page.js) +- [examples/app.js](./examples/app.js) +- [examples/parent-child.js](./examples/parent-child.js) diff --git a/guide/actions-and-docs.md b/guide/actions-and-docs.md new file mode 100644 index 0000000..8e9f85b --- /dev/null +++ b/guide/actions-and-docs.md @@ -0,0 +1,162 @@ +# Actions And Docs + +This guide is about registering component actions and document event handlers with the `DOCS` system. + +## Using `DOCS` inside a component + +Import `DOCS` and initialize it with the module filename and instance `sid`: + +```js +const DOCS = require('DOCS') + +async function component (opts, invite) { + const docs = DOCS(__filename)(opts.sid) + // ... +} +``` + +### Wrapping isolated handlers + +Pass a normal function to `docs.wrap_isolated()`. DOCS compiles it without closure access and supplies only the original `event`, callable `$`, and `$.state`. The original handler `this` is preserved, normally as the DOM element; do not store state or component resources on it. + +```js +const click_state = { count: 0 } + +function on_click (event, $) { + $.state.count += 1 + event.currentTarget.textContent = $.state.count + if ($.state.count === 10) $('Click Rate Result') +} + +on_click.info = 'Record one click in the sequence.' +on_click.opts = { state: click_state } +button.onclick = docs.wrap_isolated(on_click) +``` + +The handler may progress an interaction across several events before requesting one action. It does not receive `sdb`, `drive`, `_`, or component closures. Real effects belong in the registered action's `run` function. + +Normal mode uses the declared state and executes `run`. Docs mode blocks the browser event, uses fresh disposable state, and displays handler or action information without executing `run`. Handlers sharing the same state object participate in the same interaction sequence. + +Use `handler.info` for documentation. State belongs in `handler.opts.state`, never on a DOM element. + +`wrap_isolated` accepts normal functions only. It is the only DOCS event-handler wrapper and does not expose `sdb`, `drive`, `_`, or other component resources. + +### Browsing docs without gestures + +`docs.get_toc()` (admin: `docs.admin.get_toc(sid)`) returns `{ actions, handlers }` so a details UI can list every action `info` and handler doc for a component without clicking: + +```js +const { actions, handlers } = docs.get_toc() +// handlers: [{ doc, component }, ...] recorded from wrap_isolated +``` + +The registry dedupes handler documentation, so re-rendering a dynamic list does not add duplicate entries. Call `docs.clear_handler_docs()` (admin: `docs.admin.clear_handler_docs(sid)`) to reset a component's handler docs on teardown or re-init. + +--- + +## How the ❔ details window works + +The details window leverages a global docs mode state: + +1. Docs mode is activated globally. +2. DOCS prevents the browser default and propagation for a function-based isolated handler. +3. The handler runs with disposable interaction state. +4. If it requests an action, DOCS displays `action.info` instead of calling `run`; otherwise it displays the handler documentation. +5. The display handler receives `{ content, sid }` and renders the content. +6. `docs.get_toc()` lets the UI browse all actions and handler docs without triggering a gesture. + +### Admin Setup (Root Module) + +Only the first caller (the root module) gets the admin API: + +```js +const docs = DOCS(__filename)(opts.sid) + +// Toggle docs mode +docs.admin.set_docs_mode(true) + +// Set the display callback +docs.admin.set_doc_display_handler(({ content, sid }) => { + // Render details UI with content +}) +``` + +--- + +## Action Registration for the ActionBar + +Components register their available administrative/user actions using `docs.register_actions(actions_list)`. + +### Action Schema + +Each action must follow this shape: + +```json +{ + "name": "Action Name", + "info": "Explain what this action does when it is triggered.", + "icon": "icon_identifier", + "status": { + "pinned": true, + "default": false + }, + "steps": [ + { + "name": "Step Name", + "type": "mandatory", + "is_completed": false, + "component": "form_input", + "status": "default", + "data": "" + } + ] +} +``` + +`info` is required. Keep it short and useful because docs mode displays this text when the action would normally run. A component-owned action may also include a `run` closure; DOCS stores it privately and omits it from public action metadata. + +Use `status.hidden: true` for internal component operations that must remain dispatchable without appearing in action menus. Hidden actions are omitted from `get_actions()` and the ToC action list. + +### Registering actions + +Load the actions array from the component drive and register: + +```js +const actions_file = await drive.get('actions/commands.json') +if (actions_file.raw) { + const actions = JSON.parse(actions_file.raw) + docs.register_actions(actions) +} +``` + +For a component-owned action, register its real closure with the metadata: + +```js +const save_action = { + name: 'Save', + info: 'Save the current document.', + icon: 'save', + status: {}, + steps: [], + run: save +} + +docs.register_actions([save_action]) +``` + +An isolated handler requests it by name or generated alias: + +```js +function on_save (event, $) { $('save') } +``` + +DOCS alone decides whether to show `info` or call `run`. Components do not perform their own docs-mode gate. + +### Retrieving actions (ActionBar/Admin) + +The root module uses the admin API to retrieve registered actions for the focused app: + +```js +const actions = docs.admin.get_actions(focused_sid) +// Pass actions to action_bar component +``` diff --git a/guide/cheat-sheet.md b/guide/cheat-sheet.md new file mode 100644 index 0000000..7b14948 --- /dev/null +++ b/guide/cheat-sheet.md @@ -0,0 +1,121 @@ +# Cheat Sheet + +Use this when you need the shortest working overview of this repository's component style. + +## Component shape + +Reusable components live under `src/node_modules/*`. + +```js +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(defaults) + +module.exports = component + +async function component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = '
' + + await sdb.watch(onbatch) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + console.log(type, data) + } + } +} +``` + +## Defaults and api + +Use `defaults` for module defaults. + +Use nested `api` for instance customization. + +```js +function defaults () { + return { + api, + drive: { + 'style/': { + 'theme.css': { + raw: '.component { display: flex; }' + } + } + } + } + + function api () { + return { + drive: { + 'style/': {} + } + } + } +} +``` + +Older source files may call these functions `fallback_module` and `fallback_instance`. + +## Datasets + +Datasets use folder names with trailing slashes. + +```js +drive: { + 'icons/': { + 'close.svg': { + $ref: 'close.svg' + } + } +} +``` + +Use `$ref` for CSS, SVG, or larger assets near the module file. + +## Messaging + +Use `net_helper` when a component talks to a parent or child. + +```js +const net = require('net_helper') +const { io, _ } = net(id) + +io.on = { + up: io_up() +} +if (invite) io.accept(invite) + +button.onclick = onbutton_click + +function onbutton_click () { + _.up('button_clicked', {}, { value: true }) +} +``` + +Channel helpers use this signature: + +```js +_[name](type, refs = {}, data = null) +``` + +Use `{}` for root or UI events. + +Use `{ cause: msg.head }` when a message is caused by another message. + +## Read next + +- Strict rules: [coding-standards.md](./coding-standards.md) +- Create a component: [create-component.md](./create-component.md) +- STATE and datasets: [datashell/state.md](./datashell/state.md) +- Protocol: [datashell/protocol.md](./datashell/protocol.md) +- Actions & Docs: [actions-and-docs.md](./actions-and-docs.md) + diff --git a/guide/coding-standards.md b/guide/coding-standards.md new file mode 100644 index 0000000..c82ead0 --- /dev/null +++ b/guide/coding-standards.md @@ -0,0 +1,82 @@ +# Coding Standards + +Use this as the strict rule set for component code and guide examples. + +## Base rules + +- Use CommonJS: `require`, `module.exports`. +- Use StandardJS / standardx style. +- Use 2-space indentation. +- Use `snake_case` where practical. +- Prefer named functions. +- Do not use classes for components. +- Do not use `this` for components. +- Keep changes scoped to the task. +- Modernize touched old code paths when safe. +- Do not invent workflow commands. +- Do not create a new test setup by default. + +## Component rules + +- Reusable components use instance-level STATE with `get(opts.sid)`. +- Root pages and demo pages may use module-level STATE. +- Build UI with JavaScript and template literals. +- Use closed shadow DOM: `el.attachShadow({ mode: 'closed' })`. +- Use property handlers such as `onclick`, `oninput`, and `onchange`. +- Use `addEventListener` only when an API requires it. +- Keep render and behavior separated into named functions. +- Keep main setup above `return el`. +- Keep helper functions used by the component below `return el`. +- Keep `defaults` outside the component function. + +## STATE rules + +- Use `defaults` for module defaults. +- Use nested `api` for instance customization. +- Use `await sdb.watch(onbatch)`. +- Read batch entries through `paths`. +- Load dataset files with `drive.get(path).then(file => file.raw)`. +- Use trailing slashes for dataset names. +- Use `$ref` for bulky CSS, SVG, and asset content. +- Use `drive.put()` for persisted UI-affecting data updates. +- Use flags when a write should not trigger the full UI update flow. + +## Submodule and mapping rules + +- `_` defines submodules and instances. +- Module-level submodule declarations must include `$`. +- Instance mappings must include `mapping` when datasets pass to child modules. +- Keep dataset names aligned with child expectations. +- Empty datasets are acceptable when needed only for mapping. + +## Protocol rules + +- Use `const { io, _ } = net(id)` for `net_helper`. +- Register handlers on `io.on` using instantiating functions named `io_...` (e.g., `io_up()`) that return the handler. +- Accept parent wiring with `if (invite) io.accept(invite)`. +- Send messages with channel helpers on `_`. +- Use `_.channel(type, refs, data)`. +- Use `{}` for root or UI-originated messages. +- Use `{ cause: msg.head }` for messages derived from another message. +- Route incoming messages through action maps. +- Forward only when a wrapper translates, filters, enriches, or bridges messages. + +## Avoid + +- No old protocol callback patterns. +- No old `{ type, data }` `onbatch` examples. +- No reusable-component module-level STATE. +- No manual construction of `{ head, refs, type, data }`. +- No manual assignment of channel helpers onto `_`. +- No `switch` for message routing. +- No generic forwarding just to move messages around. +- No optional chaining unless runtime optionality is intentional. + +## Compatibility notes + +Older source files may use `fallback_module` for `defaults`. + +Older source files may use `fallback_instance` for `api`. + +Guide docs and new examples should use `defaults` and `api`. + diff --git a/guide/create-component.md b/guide/create-component.md new file mode 100644 index 0000000..08338c1 --- /dev/null +++ b/guide/create-component.md @@ -0,0 +1,73 @@ +# Create A Component + +Use this when creating a reusable component under `src/node_modules/*`. + +Read [coding-standards.md](./coding-standards.md) first. + +## Default pattern + +Reusable components use instance-level STATE. The basic structure looks like: + +```js +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(defaults) +const net = require('net_helper') + +module.exports = component + +async function component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + const { io, _ } = net(id) + + // ... setup DOM, protocols, watch drive state, return element ... +} +``` + +See a fully working component implementation in [examples/component.js](./examples/component.js). + +## Add datasets + +Define persistent files in `defaults` or `api`. + +```js +function defaults () { + return { + api, + drive: { + 'style/': { + 'theme.css': { + raw: '.button { display: inline-flex; }' + } + } + } + } + + function api () { + return { + drive: { + 'style/': {} + } + } + } +} +``` + +Use [datashell/state.md](./datashell/state.md) for the full STATE rules. + +## Add parent communication + +Use `net_helper` when the component sends events to a parent or receives commands. + +```js +function onbutton_click () { + _.up('button_clicked', {}, { value: true }) +} + +function handle_render (msg) { + _.up('rendered', { cause: msg.head }, { ok: true }) +} +``` + +Use [datashell/protocol.md](./datashell/protocol.md) for parent/child wiring. diff --git a/guide/datashell/README.md b/guide/datashell/README.md new file mode 100644 index 0000000..777613b --- /dev/null +++ b/guide/datashell/README.md @@ -0,0 +1,27 @@ +# Data Shell + +Use this section for STATE, datasets, mappings, and component messaging. + +## Read by task + +- Persistent component data: read [state.md](./state.md). +- Parent/child communication: read [protocol.md](./protocol.md). +- Exact router API behavior: read [net-helper.md](./net-helper.md). + +## Core terms + +- `STATE`: creates state database access for a module. +- `statedb`: the module-scoped state database function. +- `defaults`: module-level default state factory. +- `api`: instance customization factory returned by `defaults`. +- `sdb`: state database for the current node or instance. +- `drive`: persistent data attached to the current node. +- dataset: a named folder in `drive`, such as `style/`. +- mapping: parent-to-child dataset connection. +- invite: wiring object passed from parent to child. +- channel helper: send function created on `_` by `net_helper`. + +Older source files may call `defaults` `fallback_module`. + +Older source files may call `api` `fallback_instance`. + diff --git a/guide/datashell/net-helper.md b/guide/datashell/net-helper.md new file mode 100644 index 0000000..798f346 --- /dev/null +++ b/guide/datashell/net-helper.md @@ -0,0 +1,111 @@ +# Net Helper + +Use this when you need the exact `net_helper` API or router behavior. + +For normal component communication, start with [protocol.md](./protocol.md). + +## API + +```js +const net = require('net_helper') + +const { io, _ } = net(id) + +io.on = { + up: io_up(), + child: io_child() +} +``` + +`net(id)` returns: + +- `io.invite(name, ids)` +- `io.accept(invite)` +- `io.on` +- `_` + +## Invite and accept + +A parent creates an invite for a child. + +```js +const child = await child_component({ ...subs[0] }, io.invite('child', { up: id })) +``` + +The child accepts the invite. + +```js +async function child_component (opts, invite) { + const { id } = await get(opts.sid) + const { io, _ } = net(id) + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) +} +``` + +After `invite` and `accept`, `net_helper` creates channel helpers on `_`. + +## Channel helpers + +Each helper sends a message through a named channel. + +```js +const head = _.child('render', { cause: msg.head }, msg.data) +``` + +Signature: + +```js +_[name](type, refs = {}, data = null) +``` + +The helper creates: + +- `head` +- `refs` +- `type` +- `data` +- `meta.time` +- `meta.stack` + +The helper returns the generated `head`. + +Keep the returned `head` only when you need to match a later response. + +## Message shape + +Messages routed by `net_helper` have this shape: + +```js +{ + head: [by, to, mid], + refs: { cause: parent_message_head }, + type: 'message_type', + data: {}, + meta: { + time, + stack + } +} +``` + +`head` (representing `[sender_id, receiver_id, message_id]`) and `meta` are managed by `net_helper`. + +Callers provide `type`, `refs`, and `data`. + +## Routing behavior + +`net_helper` routes by the recipient in `head`. + +If the recipient is not the current component, the router forwards the message through known connected channels. + +Components should still use channel helpers directly instead of rebuilding message objects. + +```js +_.action_bar(msg.type, { cause: msg.head }, msg.data) +_.up('render_form', { cause: msg.head }, data) +``` + diff --git a/guide/datashell/protocol.md b/guide/datashell/protocol.md new file mode 100644 index 0000000..aafcf57 --- /dev/null +++ b/guide/datashell/protocol.md @@ -0,0 +1,109 @@ +# Protocol + +Use this when components communicate with parents or children. + +The component communication pattern uses `net_helper`, `invite` / `accept`, `io.on`, and channel helpers on `_`. + +For the exact helper API, read [net-helper.md](./net-helper.md). + +## Component pattern + +Every component registers message handlers on `io.on` using instantiating functions (like `io_up()`) and accepts the parent invite if present: + +```js +io.on = { + up: io_up() +} +if (invite) io.accept(invite) +``` + +See the full setup under `async function component` in [examples/component.js](../examples/component.js). + +Channel helpers use this signature: + +```js +_[name](type, refs = {}, data = null) +``` + +Messages contain a `head` array of structure `[by, to, mid]` representing sender (`by`), receiver (`to`), and message identifier (`mid`). + +Use `{}` for root or UI-originated messages. + +```js +function onbutton_click () { + _.up('button_clicked', {}, { value: true }) +} +``` + +Use `{ cause: msg.head }` for messages caused by another message. + +```js +function handle_request (msg) { + _.up('request_done', { cause: msg.head }, { ok: true }) +} +``` + +Do not manually build `head`, `refs`, `type`, `data`, or `meta`. + +## Parent to child + +```js +const { io, _ } = net(id) + +io.on = { + child: io_child() +} + +const child = await child_component({ ...subs[0] }, io.invite('child', { up: id })) + +function io_child () { + return function child_protocol (msg) { + const handler = child_messages[msg.type] || fail + handler(msg) + } +} + +function render_child (msg) { + _.child('render', msg.head ? { cause: msg.head } : {}, msg.data) +} +``` + +See [../examples/parent-child.js](../examples/parent-child.js) for a complete example. + +## Route messages + +Use action maps. + +```js +const on_message = { + load: handle_load, + save: handle_save +} + +function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) +} +``` + +Do not use `switch` for message routing. + +## Forwarding + +Do not forward messages just to move them through already connected components. + +Use the correct connected `_` helper directly. + +Forward only when a wrapper intentionally: + +- translates message type or data +- filters messages +- enriches payload +- bridges components that do not directly share channel helpers + +## Avoid + +- No old callback protocol style. +- No manual channel helper assignment onto `_`. +- No manual message object construction. +- No incorrect `_[name](type, data, refs)` helper argument order. diff --git a/guide/datashell/state.md b/guide/datashell/state.md new file mode 100644 index 0000000..8971b39 --- /dev/null +++ b/guide/datashell/state.md @@ -0,0 +1,180 @@ +# STATE And Data + +Use this when working with `STATE`, `sdb`, `drive`, `defaults`, `api`, datasets, mappings, or `sdb.watch`. + +## Setup + +```js +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(defaults) +``` + +Reusable components fetch instance state inside the component. + +```js +async function component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb +} +``` + +Root pages and demo pages may use module-level STATE. + +```js +const { sdb, id } = statedb(defaults) +``` + +## Watch datasets + +Use `await sdb.watch(onbatch)`. + +Batch entries contain `type` and `paths`. + +```js +const on = { + style: inject, + icons: iconject +} + +await sdb.watch(onbatch) + +async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const handler = on[type] || fail + handler(data, type) + } +} +``` + +Handlers receive dataset file contents as `data`. + +Do not use the old `{ type, data }` batch shape. + +## Drive + +`drive` stores persistent data attached to the current node. + +Use datasets with trailing slashes. + +```js +drive: { + 'style/': { + 'theme.css': { + raw: ` + .component { + display: flex; + } + ` + } + } +} +``` + +Use `$ref` for larger CSS, SVG, or asset content near the module. + +```js +drive: { + 'icons/': { + 'close.svg': { + $ref: 'close.svg' + } + } +} +``` + +## Defaults and api + +Use `defaults` for module defaults. + +Use nested `api` for instance customization. + +```js +function defaults () { + return { + api, + _: { + child_component: { + $: '' + } + } + } + + function api () { + return { + _: { + child_component: { + 0: '', + mapping: { + style: 'style' + } + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: '.component { display: flex; }' + } + } + } + } + } +} +``` + +## `_` and mapping + +`_` defines submodules and instances. + +Module-level submodule declarations must include `$`. + +```js +_: { + child_component: { + $: '' + } +} +``` + +Instance mappings must include `mapping` when datasets pass to child modules. + +```js +_: { + child_component: { + 0: '', + mapping: { + style: 'parent_style' + } + } +} +``` + +The mapping key is the child dataset name. + +The mapping value is the parent dataset name. + +Empty parent datasets are acceptable when they exist only for mapping. + +```js +drive: { + 'parent_style/': {} +} +``` + +## Drive updates + +Use `drive.put()` for persisted data updates that affect UI. + +`drive.put()` triggers `onbatch`. + +Use flags when a write should not cause the normal UI update path. + +## Compatibility notes + +Older source files may call `defaults` `fallback_module`. + +Older source files may call `api` `fallback_instance`. + +Guide examples should use `defaults` and `api`. + diff --git a/guide/demo-pages.md b/guide/demo-pages.md new file mode 100644 index 0000000..88f8e3d --- /dev/null +++ b/guide/demo-pages.md @@ -0,0 +1,52 @@ +# Demo Pages + +Use this when creating a browser preview page. + +Create two modules: + +- `web/page.js`: minimal browser entry that configures the document and renders the app. +- `src/app.js`: app setup module that creates the UI and wires components. + +`app.js` may also live under `src/node_modules` when you want to package the app like the other local modules. + +## page.js + +Keep `page.js` small. It should not contain component setup, STATE defaults, dataset mappings, or protocol wiring. It should only configure the document and boot the app module. + +See the complete example in [examples/page.js](./examples/page.js). + +## app.js + +Use `app.js` for the actual app setup. It acts as the root module, initializing the router API, mounting children, and watching root datasets. + +See the complete example in [examples/app.js](./examples/app.js). + +## Defaults + +`app.js` defines child instances in `defaults`. + +```js +function defaults () { + return { + _: { + component: { + $: '', + 0: '', + mapping: { + style: 'style' + } + } + }, + drive: { + 'style/': {} + } + } +} +``` + +## Point of View + +- Keep `web/page.js` minimal. +- Put app setup in `app.js`. +- Use module-level STATE in `app.js` only when it is the root app module. +- Use `get(opts.sid)` inside reusable components. diff --git a/guide/examples/app.js b/guide/examples/app.js new file mode 100644 index 0000000..5783224 --- /dev/null +++ b/guide/examples/app.js @@ -0,0 +1,81 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { sdb, id } = statedb(defaults) +const net = require('net_helper') + +const status_button = require('status_button') + +module.exports = app + +async function app () { + const { io, _ } = net(id) + + const on = { + style: inject + } + + const action_handlers = { + status_clicked: handle_status_clicked + } + + io.on = { + status_button: io_status_button() + } + + const subs = await sdb.watch(onbatch) + const el = await status_button({ ...subs[0] }, io.invite('status_button', { up: id })) + + return el + + async function onbatch (batch) { + const { drive } = sdb + + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const handler = on[type] || fail + handler(data, type) + } + } + + function io_status_button () { + return function status_button_protocol (msg) { + const handler = action_handlers[msg.type] || fail + handler(msg) + } + } + + function handle_status_clicked (msg) { + _.status_button('set_label', { cause: msg.head }, { label: msg.data.active ? 'Active' : 'Ready' }) + } + + function inject (data) { + const sheet = new CSSStyleSheet() + sheet.replaceSync(data.join('\n')) + document.adoptedStyleSheets = [sheet] + } + + function fail (data, type) { + console.warn(__filename + ' invalid message', { cause: { data, type } }) + } +} + +function defaults () { + return { + _: { + status_button: { + $: '', + 0: '', + mapping: { + style: 'style' + } + } + }, + drive: { + 'style/': { + 'page.css': { + raw: 'body { margin: 16px; }' + } + } + } + } +} diff --git a/guide/examples/component.js b/guide/examples/component.js new file mode 100644 index 0000000..ff7b630 --- /dev/null +++ b/guide/examples/component.js @@ -0,0 +1,109 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(defaults) +const net = require('net_helper') + +module.exports = status_button + +async function status_button (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + const { io, _ } = net(id) + + const on = { + style: inject, + label: onlabel + } + + const on_message = { + set_label: handle_set_label + } + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = '' + + const button = shadow.querySelector('.status-button') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + button.onclick = onbutton_click + + await sdb.watch(onbatch) + + return el + + function onbutton_click () { + if (_.up) _.up('status_clicked', {}, { active: button.classList.toggle('active') }) + } + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function onmessage_fail (msg) { + fail(msg.data, msg.type) + } + + function handle_set_label (msg) { + button.textContent = msg.data.label + if (_.up) _.up('label_updated', { cause: msg.head }, { label: msg.data.label }) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const handler = on[type] || fail + handler(data, type) + } + } + + function inject (data) { + sheet.replaceSync(data.join('\n')) + } + + function onlabel (data) { + button.textContent = data[0] + } + + function fail (data, type) { + console.warn(__filename + ' invalid message', { cause: { data, type } }) + } +} +function defaults () { + return { + api, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .status-button { + border: 1px solid #999; + padding: 6px 10px; + } + ` + } + } + } + } + + function api () { + return { + drive: { + 'label/': { + 'text.txt': { + raw: 'Ready' + } + }, + 'style/': {} + } + } + } +} diff --git a/guide/examples/page.js b/guide/examples/page.js new file mode 100644 index 0000000..ce04047 --- /dev/null +++ b/guide/examples/page.js @@ -0,0 +1,21 @@ +const app = require('../src/app') +config().then(boot_default_page) + +async function config () { + const html = document.documentElement + const meta = document.createElement('meta') + const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const loadFont = `` + + html.setAttribute('lang', 'en') + meta.setAttribute('name', 'viewport') + meta.setAttribute('content', 'width=device-width,initial-scale=1.0') + document.head.append(meta) + document.head.insertAdjacentHTML('beforeend', loadFont) + + await document.fonts.ready +} + +async function boot_default_page () { + document.body.append(await app()) +} diff --git a/guide/examples/parent-child.js b/guide/examples/parent-child.js new file mode 100644 index 0000000..cfe46a0 --- /dev/null +++ b/guide/examples/parent-child.js @@ -0,0 +1,125 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(defaults) +const net = require('net_helper') + +const child_component = require('child_component') + +module.exports = parent_component + +async function parent_component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + const { io, _ } = net(id) + + const on = { + style: inject + } + + const child_messages = { + child_ready: handle_child_ready, + child_changed: handle_child_changed + } + + const parent_messages = { + set_child: handle_set_child + } + + io.on = { + up: io_up(), + child: io_child() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = '
' + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const placeholder = shadow.querySelector('child-placeholder') + + const subs = await sdb.watch(onbatch) + const child = await child_component({ ...subs[0] }, io.invite('child', { up: id })) + placeholder.replaceWith(child) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = parent_messages[msg.type] || onmessage_fail + handler(msg) + } + } + + function io_child () { + return function child_protocol (msg) { + const handler = child_messages[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_child_ready (msg) { + _.child('render', { cause: msg.head }, { status: 'ready' }) + } + + function handle_child_changed (msg) { + if (_.up) _.up('child_changed', { cause: msg.head }, msg.data) + } + + function handle_set_child (msg) { + _.child('render', { cause: msg.head }, msg.data) + } + + function onmessage_fail (msg) { + fail(msg.data, msg.type) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(path => drive.get(path).then(file => file.raw))) + const handler = on[type] || fail + handler(data, type) + } + } + + function inject (data) { + sheet.replaceSync(data.join('\n')) + } + + function fail (data, type) { + console.warn(__filename + ' invalid message', { cause: { data, type } }) + } +} + +function defaults () { + return { + api, + _: { + child_component: { + $: '' + } + } + } + + function api () { + return { + _: { + child_component: { + 0: '', + mapping: { + style: 'child_style' + } + } + }, + drive: { + 'style/': { + 'parent.css': { + raw: '.parent { display: grid; gap: 8px; }' + } + }, + 'child_style/': {} + } + } + } +} diff --git a/guide/theme-widget.md b/guide/theme-widget.md new file mode 100644 index 0000000..ca3710f --- /dev/null +++ b/guide/theme-widget.md @@ -0,0 +1,60 @@ +# Theme Widget + +Use this when changing theme widget styling, datasets, mappings, or dependency wiring. + +`theme_widget` is an application-level component used to modify theme data for other modules. + +Theme work usually means finding which child module owns the visual element, then mapping or updating the right dataset through parent fallbacks. + +## Dependency tree + +```text +theme_widget + program_container + console_history + actions + tabbed_editor + graph_viewer + docs_window + taskbar + action_executor + program + steps_wizard + form_input + input_test + action_bar + quick_actions + tabsbar + tabs + task_manager +``` + +## Workflow + +1. Find the module that owns the target visual element. +2. Inspect its `defaults` and `api` drive datasets. +3. Check parent `_` mappings from `theme_widget` down to that module. +4. Add or adjust datasets where mapping requires them. +5. Use `$ref` for bulky CSS or SVG assets. +6. Keep mapping names aligned with child expectations. + +## Source links + +- [`theme_widget`](../src/node_modules/theme_widget/theme_widget.js) +- [`program_container`](../src/node_modules/program_container/program_container.js) +- [`console_history`](../src/node_modules/console_history/console_history.js) +- [`actions`](../src/node_modules/actions/actions.js) +- [`tabbed_editor`](../src/node_modules/tabbed_editor/tabbed_editor.js) +- [`graph_viewer`](../src/node_modules/graph_viewer/graph_viewer.js) +- [`docs_window`](../src/node_modules/docs_window/docs_window.js) +- [`taskbar`](../src/node_modules/taskbar/taskbar.js) +- [`action_executor`](../src/node_modules/action_executor/action_executor.js) +- [`program`](../src/node_modules/program/program.js) +- [`steps_wizard`](../src/node_modules/steps_wizard/steps_wizard.js) +- [`form_input`](../src/node_modules/form_input/form_input.js) +- [`input_test`](../src/node_modules/input_test/input_test.js) +- [`action_bar`](../src/node_modules/action_bar/action_bar.js) +- [`quick_actions`](../src/node_modules/quick_actions/quick_actions.js) +- [`tabsbar`](../src/node_modules/tabsbar/tabsbar.js) +- [`tabs`](../src/node_modules/tabs/tabs.js) +- [`task_manager`](../src/node_modules/task_manager/task_manager.js) diff --git a/guide/use-existing-component.md b/guide/use-existing-component.md new file mode 100644 index 0000000..74d576c --- /dev/null +++ b/guide/use-existing-component.md @@ -0,0 +1,95 @@ +# Use An Existing Component + +Use this when extracting a component from `src/node_modules/*` and mounting it in another app or parent component. + +## Create an instance + +Parent components create child instances through `_` in `defaults` or `api`. + +```js +const child_component = require('child_component') + +function defaults () { + return { + api, + _: { + child_component: { + $: '' + } + } + } + + function api () { + return { + _: { + child_component: { + 0: '', + mapping: { + style: 'child_style' + } + } + }, + drive: { + 'child_style/': {} + } + } + } +} +``` + +## Mount the child + +Call `sdb.watch(onbatch)` to get child instance SIDs. + +```js +const subs = await sdb.watch(onbatch) +const child = await child_component({ ...subs[0] }) +placeholder.replaceWith(child) +``` + +## Wire messages when needed + +If the child uses `net_helper`, pass an invite. + +```js +const { io, _ } = net(id) + +io.on = { + child: io_child() +} + +const child = await child_component({ ...subs[0] }, io.invite('child', { up: id })) + +function io_child () { + return function child_protocol (msg) { + const handler = child_messages[msg.type] || fail + handler(msg) + } +} +``` + +Use the connected channel helper to send to the child. + +```js +function render_child (msg) { + _.child('render', msg.head ? { cause: msg.head } : {}, msg.data) +} +``` + +## Map datasets + +Use `mapping` when the parent passes datasets to the child. + +```js +mapping: { + style: 'child_style', + icons: 'child_icons' +} +``` + +The key is the child dataset name. + +The value is the parent dataset name. + +Read [datashell/state.md](./datashell/state.md) before changing dataset names. + diff --git a/index.html b/index.html new file mode 100644 index 0000000..0ac2213 --- /dev/null +++ b/index.html @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/index.js b/index.js new file mode 100644 index 0000000..67ef100 --- /dev/null +++ b/index.js @@ -0,0 +1,6 @@ +const env = { version: 'latest' } +const arg = { x: 321, y: 543 } +const url = 'https://playproject.io/datashell/shim.js' +const src = `${url}?${new URLSearchParams(env)}#${new URLSearchParams(arg)}` +// eslint-disable-next-line no-undef +this.open ? document.body.append(Object.assign(document.createElement('script'), { src })) : importScripts(src) diff --git a/package.json b/package.json new file mode 100644 index 0000000..e7c9748 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "ui-components", + "version": "0.0.2-pre-alpha", + "description": "An app for debuging and testing website's theme. It is a simple app that allows you to change the theme of a website and see the changes in real time. It has 4 components: action-bar(action-wizard),graph-explorer,tabbed-editor,action-history", + "homepage": "https://github.com/ddroid/ui-components#readme", + "bugs": { + "url": "https://github.com/ddroid/ui-components/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ddroid/ui-components.git" + }, + "license": "ISC", + "author": "Ahmad Munir", + "type": "commonjs", + "main": "src/index.js", + "scripts": { + "start": "budo web/page.js:bundle.js --dir . --live --open -- -i STATE", + "build": "browserify web/page.js -i STATE -o bundle.js", + "lint": "standardx --fix", + "test": "vitest run" + }, + "devDependencies": { + "browserify": "^17.0.1", + "budo": "^11.8.4", + "standardx": "^7.0.0", + "vitest": "^1.2.0" + }, + "eslintConfig": { + "env": { + "browser": true + }, + "rules": { + "camelcase": 0, + "indent": [ + "error", + 2 + ] + } + }, + "standardx": { + "ignore": [ + "**/node_modules/**", + "**/bundle.js", + "!**/src/node_modules/**" + ] + }, + "dependencies": { + "graph-explorer": "github:ddroid/graph-explorer" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..d777df2 --- /dev/null +++ b/src/index.js @@ -0,0 +1 @@ +module.exports = require('ui_gallery') diff --git a/src/node_modules/DEBUG/README.md b/src/node_modules/DEBUG/README.md new file mode 100644 index 0000000..652bc3b --- /dev/null +++ b/src/node_modules/DEBUG/README.md @@ -0,0 +1,42 @@ +# DEBUG + +A tiny global feature-flag / debugging registry shared across every component +instance in the app. Same architectural shape as [`DOCS`](../DOCS/README.md): a +single singleton lives on `window` (or `global` in a worker), so flipping a flag +in one component is instantly visible to all of them. + +## Usage + +```js +const DEBUG = require('DEBUG') +const debug = DEBUG(__filename)(opts.sid) + +// read +if (debug.get_flag('show_default_entries')) { /* ... */ } + +// react to changes (returns an unsubscribe fn) +const off = debug.on_change((name, value) => render()) + +// toggle from anywhere (e.g. a debug button or the devtools console) +debug.set_flag('show_default_entries', false) +``` + +From the browser devtools console you can also flip flags directly: + +```js +window.__DEBUG_GLOBAL_STATE__.flags.show_default_entries = false +``` + +(note: setting the object directly does NOT notify subscribers — prefer +`debug.set_flag(...)` so components re-render). + +## Adding a new flag + +Declare its default in `DEFAULT_FLAGS` inside [index.js](./index.js). Every +component can then read/subscribe to it with no extra plumbing. + +## Current flags + +| flag | default | used by | meaning | +| --- | --- | --- | --- | +| `show_default_entries` | `true` | `console_history` | render static demo/seed history entries | diff --git a/src/node_modules/DEBUG/index.js b/src/node_modules/DEBUG/index.js new file mode 100644 index 0000000..82e474d --- /dev/null +++ b/src/node_modules/DEBUG/index.js @@ -0,0 +1,48 @@ +module.exports = function DEBUG (filename) { + return function (sid) { return create_context(filename, sid) } +} + +const DEFAULT_FLAGS = { + show_default_entries: true +} + +const scope = typeof window !== 'undefined' ? window : global + +if (!scope.__DEBUG_GLOBAL_STATE__) { + scope.__DEBUG_GLOBAL_STATE__ = { + flags: { ...DEFAULT_FLAGS }, + listeners: [] + } +} + +const state = scope.__DEBUG_GLOBAL_STATE__ + +function set_flag (name, value) { + if (state.flags[name] === value) return + state.flags[name] = value + state.listeners.forEach(notify) + + function notify (listener) { listener(name, value) } +} + +function get_flag (name) { return state.flags[name] } + +function get_flags () { return { ...state.flags } } + +function on_change (listener) { + state.listeners.push(listener) + return unsubscribe + + function unsubscribe () { state.listeners = state.listeners.filter(keep) } + function keep (l) { return l !== listener } +} + +function create_context (filename, sid) { + return { + get_flag, + get_flags, + set_flag, + on_change, + meta: { component: filename, sid } + } +} diff --git a/src/node_modules/DOCS/README.md b/src/node_modules/DOCS/README.md new file mode 100644 index 0000000..4edcc61 --- /dev/null +++ b/src/node_modules/DOCS/README.md @@ -0,0 +1,178 @@ +# DOCS Module + +Documentation-mode and action registration system for UI components. + +`DOCS` is called as `DOCS(filename)(sid)`. The returned context records `filename` as component metadata and registers actions under `sid`. + +```js +const DOCS = require('DOCS') +const docs = DOCS(__filename)(opts.sid) +``` + +## API + +Every context includes: + +```js +docs.wrap_isolated(handler) +docs.get_docs_mode() +docs.on_docs_mode_change(listener) +docs.get_toc() +docs.clear_handler_docs() +docs.register_actions(actions) +``` + +`wrap_isolated` accepts normal functions only. DOCS does not expose component resources or a non-isolated wrapper. + + +Only the first created context also includes `docs.admin`: + +```js +docs.admin.set_docs_mode(active) +docs.admin.set_doc_display_handler(callback) +docs.admin.get_actions(sid) +docs.admin.get_toc(sid) +docs.admin.clear_handler_docs(sid) +docs.admin.list_registered() +``` + +## Handlers + +### `docs.wrap_isolated(handler)` + +Pass a normal function. DOCS compiles it without closure access and calls it as `(event, $)`. + +```js +const click_state = { count: 0 } + +function on_click (event, $) { + $.state.count += 1 + event.currentTarget.textContent = $.state.count + if ($.state.count === 10) $('Click Rate Result') +} + +on_click.info = 'Record one click in the sequence.' +on_click.opts = { state: click_state } +button.onclick = docs.wrap_isolated(on_click) +``` + +The isolated interface is intentionally small: + +- `event` is the original event. +- `this` remains the original handler receiver, normally the DOM element. Do not store interaction state or component resources on it. +- `$.state` is the active interaction state. +- `$('Action Name')` requests one registered action. + +Handlers do not receive `sdb`, `drive`, `_`, or component closures. Put real effects in the registered action's `run` function, which keeps normal closure access. + +In normal mode, the handler uses its declared state and DOCS invokes `run` when requested. In docs mode, DOCS blocks the browser event, runs the handler with disposable state, and shows handler information or action information without invoking `run`. + +Set documentation on `handler.info`. Declared state must be a plain cloneable object. Handlers that reference the same state object share one interaction sequence. + +## Table of Contents + +`docs.get_toc()` (and `docs.admin.get_toc(sid)`) return `{ actions, handlers }` so a details UI can browse every action `info` and every handler doc for a component without triggering gestures: + +```js +const { actions, handlers } = docs.get_toc() +// actions: registered action objects (name, info, icon, ...) +// handlers: [{ doc, component }, ...] from wrap_isolated +``` + +Handler docs are recorded from `handler.info`. Content may be a string, promise, or resolving function. + +The registry dedupes handler documentation, so re-rendering a dynamic list (which re-wraps handlers) does not grow the ToC with duplicates. For a full reset — e.g. when a component is torn down or re-initialized — call `docs.clear_handler_docs()` (or `docs.admin.clear_handler_docs(sid)`): + +```js +docs.clear_handler_docs() // current component context +docs.admin.clear_handler_docs(sid) // any sid from the root context +``` + +## Interaction state + +Normal mode uses the object from `handler.opts.state`. Each docs-mode session uses a fresh disposable copy, leaving real progress unchanged. When several handlers reference the same state object, they share the same real and disposable interaction state. + +A component that renders disposable progress can use `docs.on_docs_mode_change()` to restore its real rendering when docs mode closes. + +## Component pattern + +1. Keep interaction state in JavaScript and expose it through `handler.opts.state`. +2. Set handler documentation on `handler.info`. +3. Move real effects into an action descriptor's `run` closure. +4. Request that action from the isolated handler with `$('Action Name')`. + +Event handlers never receive `sdb`, `drive`, `_`, or component closures, and components do not perform their own docs-mode checks. + +## Docs Mode + +```js +docs.get_docs_mode() +const unsubscribe = docs.on_docs_mode_change(function onmode_change (active) {}) +``` + +The admin/root context controls global docs mode and rendering: + +```js +docs.admin.set_docs_mode(true) +docs.admin.set_doc_display_handler(({ content, sid }) => { + // Render markdown content for sid +}) +``` + +When docs content resolves empty, the display callback receives `No documentation available`. + +## Actions + +Register component actions: + +```js +docs.register_actions(actions) +``` + +Retrieve them from the admin/root context: + +```js +const actions = docs.admin.get_actions(sid) // throws if the sid has never registered actions +docs.admin.list_registered() // registered SIDs +``` + +`register_actions()` validates that actions are an array and each action has `name` string, `info` string, `icon` string, `status` object, and `steps` array. A component-owned action may also provide a `run` closure. DOCS stores `run` privately and omits it from public action metadata. + +Set `status.hidden` for internal component operations that isolated handlers must dispatch but action menus must not list. Hidden actions remain available to `$()` and Docs mode, but are omitted from `get_actions()` and the ToC action list. + +```js +{ + name: 'Action Name', + info: 'Explain what this action does when it is triggered.', + icon: 'icon_id', + status: { + pinned: true, + default: false + }, + steps: [ + { + name: 'Step Name', + type: 'mandatory', + is_completed: false, + component: 'form_input', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'search' } } + ] + } + ] +} +``` + +Common command types: `set_mode`, `set_search_query`, `set_flag`, `clear_selection`, `get_selected`. + +An isolated handler requests an action with `$('Action Name')` or its generated alias such as `$('action_name')`. DOCS displays `action.info` in docs mode and invokes `run` in normal mode. Metadata-only actions remain readable but cannot execute through the action gate. + +## Integration Flow + +1. Components load `actions/commands.json` and call `docs.register_actions(actions)`. +2. The root/admin module calls `docs.admin.get_actions(sid)` for the focused component. +3. `action_bar`, `quick_actions`, and `steps_wizard` render and execute the selected action flow. +4. In docs mode, action triggers display `info` instead of executing. +5. The root/admin module sets `docs.admin.set_doc_display_handler()` so docs-mode interactions render markdown in the details UI. diff --git a/src/node_modules/DOCS/index.js b/src/node_modules/DOCS/index.js new file mode 100644 index 0000000..5509828 --- /dev/null +++ b/src/node_modules/DOCS/index.js @@ -0,0 +1,212 @@ +module.exports = function DOCS (filename) { + return function create_docs (sid) { return create_context(filename, sid) } +} + +const scope = typeof window !== 'undefined' ? window : global + +if (!scope.__DOCS_GLOBAL_STATE__) { + scope.__DOCS_GLOBAL_STATE__ = { + docs_mode_active: false, + docs_mode_listeners: [], + doc_display_callback: null, + action_registry: new Map(), + action_lookup: new Map(), + handler_doc_registry: new Map(), + docs_interaction_state: new WeakMap() + } +} + +const state = scope.__DOCS_GLOBAL_STATE__ +state.action_lookup = state.action_lookup || new Map() +state.handler_doc_registry = state.handler_doc_registry || new Map() +state.docs_interaction_state = state.docs_interaction_state || new WeakMap() + +function set_docs_mode (active) { + if (state.docs_mode_active !== active) state.docs_interaction_state = new WeakMap() + state.docs_mode_active = active + state.docs_mode_listeners.forEach(listener => listener(active)) +} + +function get_docs_mode () { return state.docs_mode_active } + +function on_docs_mode_change (listener) { + state.docs_mode_listeners.push(listener) + return unsubscribe + + function unsubscribe () { + state.docs_mode_listeners = state.docs_mode_listeners.filter(item => item !== listener) + } +} + +function set_doc_display_handler (callback) { state.doc_display_callback = callback } + +function get_actions (sid) { + const actions = state.action_registry.get(sid) + if (!actions) throw new Error('DOCS: No actions registered for SID ' + sid) + return actions +} + +function list_registered () { return Array.from(state.action_registry.keys()) } + +function get_toc (sid) { + return { + actions: state.action_registry.get(sid) || [], + handlers: state.handler_doc_registry.get(sid) || [] + } +} + +function register_handler_doc (meta) { + if (meta.doc === undefined || meta.doc === null) return + const list = state.handler_doc_registry.get(meta.sid) || [] + if (list.some(entry => entry.doc === meta.doc)) return + list.push({ doc: meta.doc, component: meta.component }) + state.handler_doc_registry.set(meta.sid, list) +} + +function clear_handler_docs (sid) { state.handler_doc_registry.delete(sid) } + +function verify_actions (actions) { + if (!Array.isArray(actions)) throw new Error('DOCS: Actions must be array') + actions.forEach(validate_action) + + function validate_action (action, index) { + if (!action.name || typeof action.name !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'name'`) + if (!action.info || typeof action.info !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'info'`) + if (!action.icon || typeof action.icon !== 'string') throw new Error(`DOCS: Action[${index}] Invalid 'icon'`) + if (!action.status || typeof action.status !== 'object') throw new Error(`DOCS: Action[${index}] Invalid 'status'`) + if (!action.steps || !Array.isArray(action.steps)) throw new Error(`DOCS: Action[${index}] Invalid 'steps'`) + } +} + +async function display_doc (content, sid) { + let resolved_content = content + if (typeof content === 'function') { + resolved_content = await content() + } else if (content && typeof content.then === 'function') { + resolved_content = await content + } + + if (state.doc_display_callback) { + return state.doc_display_callback({ content: resolved_content || 'No documentation available', sid }) + } +} + +function wrap_isolated (handler, meta) { + if (typeof handler !== 'function') throw new TypeError('DOCS: Isolated handler must be a function') + + const isolated_handler = compile_handler(handler) + const real_state = get_handler_state(handler) + const initial_state = structuredClone(real_state) + + return async function wrapped_handler (event) { + const docs_mode = state.docs_mode_active + if (docs_mode && event) { + if (event.preventDefault) event.preventDefault() + if (event.stopPropagation) event.stopPropagation() + } + + let requested_action + const handler_state = docs_mode ? get_docs_state(real_state, initial_state) : real_state + + function $ (action) { + if (requested_action !== undefined) throw new Error('DOCS: Handler already requested an action') + if (!action || typeof action !== 'string') throw new TypeError('DOCS: Action request must be a name') + requested_action = action + } + $.state = handler_state + + const result = await isolated_handler.call(this, event, $) + if (requested_action !== undefined) return dispatch_action(meta.sid, requested_action, docs_mode) + if (docs_mode) await display_doc(meta.doc || 'No documentation available', meta.sid) + return result + } +} + +function compile_handler (handler) { + // eslint-disable-next-line no-new-func + return new Function(`return (${handler})`)() +} + +function get_handler_state (handler) { + const interaction_state = handler.opts && handler.opts.state + if (interaction_state === undefined) return {} + if (Object.getPrototypeOf(interaction_state) !== Object.prototype) { + throw new TypeError('DOCS: handler.opts.state must be a plain object') + } + structuredClone(interaction_state) + return interaction_state +} + +function get_docs_state (real_state, initial_state) { + let interaction_state = state.docs_interaction_state.get(real_state) + if (!interaction_state) { + interaction_state = structuredClone(initial_state) + state.docs_interaction_state.set(real_state, interaction_state) + } + return interaction_state +} + +function dispatch_action (sid, name, docs_mode) { + const lookup = state.action_lookup.get(sid) + const record = lookup && lookup.get(name) + if (!record) throw new Error(`DOCS: Unknown action "${name}" for SID ${sid}`) + if (docs_mode) return display_doc(record.action.info, sid) + if (!record.run) throw new Error(`DOCS: Action "${record.action.name}" has no run callback`) + return record.run() +} + +function register_actions (sid, actions) { + verify_actions(actions) + const public_actions = [] + const lookup = new Map() + + actions.forEach(register_action) + state.action_registry.set(sid, public_actions) + state.action_lookup.set(sid, lookup) + + function register_action (action) { + const { run, ...public_action } = action + if (run !== undefined && typeof run !== 'function') throw new TypeError(`DOCS: Action "${action.name}" run must be a function`) + + const record = { action: public_action, run } + const keys = new Set([action.name, action.name.toLowerCase().replace(/ /g, '_')]) + keys.forEach(register_key) + if (!action.status.hidden) public_actions.push(public_action) + + function register_key (key) { + if (lookup.has(key)) throw new Error(`DOCS: Duplicate action key "${key}" for SID ${sid}`) + lookup.set(key, record) + } + } +} + +let admin = true +function create_context (filename, sid) { + const api = { + wrap_isolated: wrap_with_component, + get_docs_mode, + on_docs_mode_change, + get_toc: () => get_toc(sid), + clear_handler_docs: () => clear_handler_docs(sid), + register_actions: actions => register_actions(sid, actions) + } + const admin_api = { + set_docs_mode, + set_doc_display_handler, + get_actions, + get_toc, + clear_handler_docs, + list_registered + } + if (admin) { + admin = false + return Object.assign({ admin: admin_api }, api) + } + return api + + function wrap_with_component (handler) { + const meta = { doc: handler && handler.info, sid, component: filename } + register_handler_doc(meta) + return wrap_isolated(handler, meta) + } +} diff --git a/src/node_modules/action_bar/README.md b/src/node_modules/action_bar/README.md new file mode 100644 index 0000000..ffb4c38 --- /dev/null +++ b/src/node_modules/action_bar/README.md @@ -0,0 +1 @@ +A container component that combines quick_actions, actions menu, and steps_wizard to provide a unified action bar interface for executing multi-step workflows. diff --git a/src/node_modules/action_bar/action_bar.js b/src/node_modules/action_bar/action_bar.js new file mode 100644 index 0000000..5ca5129 --- /dev/null +++ b/src/node_modules/action_bar/action_bar.js @@ -0,0 +1,297 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +const quick_actions = require('quick_actions') + +module.exports = action_bar + +async function action_bar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: iconject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+ +
+
+ +
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const history_icon = shadow.querySelector('.icon-btn') + const quick_placeholder = shadow.querySelector('quick-actions') + + const { io, _ } = net(id) + let console_icon = {} + const docs = DOCS(__filename)(opts.sid) + const history_action = { + name: 'Toggle Console History', + info: 'Open or close the console history.', + icon: 'console', + status: { hidden: true }, + steps: [], + run: toggle_console_history + } + docs.register_actions([history_action]) + const subs = await sdb.watch(onbatch) + + let selected_action = null + + io.on = { + up: io_up(), + quick_actions: io_quick_actions() + } + if (invite) io.accept(invite) + + history_icon.innerHTML = console_icon + on_history_click.info = history_action.info + history_icon.onclick = docs.wrap_isolated(on_history_click) + const element = await quick_actions({ ...subs[0] }, io.invite('quick_actions', { up: id })) + quick_placeholder.replaceWith(element) + + const parent_handler = { + load_actions, + selected_action: parent_selected_action, + show_submit_btn, + hide_submit_btn, + step_clicked: parent_step_clicked, + update_quick_actions_for_app, + update_quick_actions_input, + action_submitted: parent__action_submitted, + clean_up: parent__clean_up + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('Unknown message type:', type, data) } + function inject ({ data }) { sheet.replaceSync(data[0]) } + function iconject ({ data }) { console_icon = data[0] } + + // ------------------------------- + // Protocol: quick actions + // ------------------------------- + + function io_quick_actions () { + return function quick_actions_protocol (msg) { + const quick_handlers = { + display_actions: quick_actions_display_actions, + action_submitted: quick_actions_action_submitted, + filter_actions: quick_actions_filter_actions, + update_quick_actions_input, + activate_steps_wizard: quick_actions_activate_steps_wizard, + ui_focus_docs + } + + const { type } = msg + const handler = quick_handlers[type] || fail + handler(msg) + } + } + + function quick_actions_filter_actions (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function quick_actions_display_actions (msg) { + const { data } = msg + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + const display = typeof data === 'string' ? data : data.display + const reason = typeof data === 'string' ? '' : data.reason + const should_clean = display === 'none' && reason !== 'selected' + if (should_clean) { + _.up('clean_up', msg.head ? { cause: msg.head } : {}, selected_action) + } + } + + function quick_actions_action_submitted (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'completed' }) + _.up('action_submitted', msg.head ? { cause: msg.head } : {}, { selected_action }) + } + + function io_up () { + return function onmessage (msg) { + const { type } = msg + if (type === 'docs_toggle') { + _.quick_actions(type, msg.head ? { cause: msg.head } : {}, msg.data) + } else { + const handler = parent_handler[type] || fail + handler(msg) + } + } + } + + function load_actions (msg) { + // const { data } = msg + } + function parent_selected_action (msg) { + _.quick_actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function show_submit_btn (msg) { _.quick_actions('show_submit_btn', msg.head ? { cause: msg.head } : {}, null) } + function hide_submit_btn (msg) { _.quick_actions('hide_submit_btn', msg.head ? { cause: msg.head } : {}, null) } + + function update_quick_actions_for_app (msg) { + const { data, type } = msg + _.quick_actions(type, msg.head ? { cause: msg.head } : {}, data) + } + + function update_quick_actions_input (msg) { + const { data } = msg + selected_action = data || null + _.quick_actions('update_input_command', msg.head ? { cause: msg.head } : {}, data) + } + + function quick_actions_activate_steps_wizard (msg) { + _.up('activate_steps_wizard', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function parent_step_clicked (msg) { + const { data } = msg + _.quick_actions('update_current_step', msg.head ? { cause: msg.head } : {}, data) + _.up('render_form', msg.head ? { cause: msg.head } : {}, data) + } + + function parent__action_submitted (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'completed' }) + _.up('action_submitted', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function parent__clean_up (msg) { + _.quick_actions('deactivate_input_field', msg.head ? { cause: msg.head } : {}, { reason: 'cancel' }) + _.up('clean_up', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function ui_focus_docs (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function on_history_click (event, $) { $('Toggle Console History') } + function toggle_console_history () { _.up('console_history_toggle', {}, null) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + quick_actions: { $: '' }, + DOCS: { $: '' }, + net_helper: { $: '' } + } + } + function fallback_instance () { + return { + _: { + quick_actions: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + }, + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + 'console.svg': { + $ref: 'console.svg' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .container { + display: flex; + flex-direction: column; + width: 100%; + } + .action-bar-container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + background: #131315; + padding: 8px; + gap: 12px; + } + .command-history { + display: flex; + align-items: center; + } + .quick-actions { + display: flex; + flex: auto; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + min-width: 340px; + } + .hide { + display: none; + } + + .icon-btn { + display: flex; + min-width: 32px; + height: 32px; + border: none; + background: transparent; + cursor: pointer; + flex-direction: row; + justify-content: center; + align-items: center; + padding: 6px; + border-radius: 6px; + color: #a6a6a6; + } + .icon-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + svg { + width: 20px; + height: 20px; + } + ` + } + }, + 'actions/': {}, + 'hardcons/': {}, + 'prefs/': {}, + 'variables/': {} + } + } + } +} diff --git a/src/node_modules/action_bar/console.svg b/src/node_modules/action_bar/console.svg new file mode 100644 index 0000000..709fe2b --- /dev/null +++ b/src/node_modules/action_bar/console.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/action_bar/package.json b/src/node_modules/action_bar/package.json new file mode 100644 index 0000000..5e6284d --- /dev/null +++ b/src/node_modules/action_bar/package.json @@ -0,0 +1,3 @@ +{ + "main": "action_bar.js" +} \ No newline at end of file diff --git a/src/node_modules/action_executor/action_executor.js b/src/node_modules/action_executor/action_executor.js new file mode 100644 index 0000000..4ee5dde --- /dev/null +++ b/src/node_modules/action_executor/action_executor.js @@ -0,0 +1,436 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const program = require('program') +const steps_wizard = require('steps_wizard') + +const { form_input, input_test, form_tile_split_choice, form_click_rate_test } = program + +const component_modules = { + form_input, + input_test, + form_tile_split_choice, + form_click_rate_test + // Add more form input components here if needed +} + +module.exports = action_executor + +async function action_executor (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ + + +
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const program_placeholder = shadow.querySelector('program') + const form_input_placeholder = shadow.querySelector('form-input') + const steps_wizard_placeholder = shadow.querySelector('steps-wizard') + const { io, _ } = net(id) + + const subs = await sdb.watch(onbatch) + + let all_data = null + let selected_action = null + + io.on = { + up: io_up(), + program: io_program(), + steps_wizard: io_steps_wizard() + } + + // dynamic form input component SIDs + for (const [component_name] of Object.entries(component_modules)) { + // const final_index = index + 2 + io.on[component_name] = io_form_input(component_name) + } + if (invite) io.accept(invite) + + const program_el = await program({ ...subs[0] }, io.invite('program', { up: id })) + program_el.classList.add('program-bar', 'hide') + program_placeholder.replaceWith(program_el) + + const steps_wizard_el = await steps_wizard({ ...subs[1] }, io.invite('steps_wizard', { up: id })) + steps_wizard_el.classList.add('steps-wizard-bar', 'hide') + steps_wizard_placeholder.replaceWith(steps_wizard_el) + + const form_input_elements = {} + + for (const [index, [component_name, component_fn]] of Object.entries(component_modules).entries()) { + const final_index = index + 2 + const sub_entry = subs[final_index] || { sid: opts.sid } + const element = await component_fn({ ...sub_entry }, io.invite(component_name, { up: id })) + element.classList.add('form-inputs', 'hide') + form_input_elements[component_name] = element + form_input_placeholder.parentNode.insertBefore(element, form_input_placeholder) + } + + form_input_placeholder.remove() + + const parent_handler = { + update_steps_wizard_for_app, + load_actions, + action_submitted, + update_data, + activate_steps_wizard, + form_data, + render_form, + selected_action: parent_selected_action, + clean_up + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('Unknown message type:', type, data) } + function inject (data) { sheet.replaceSync(data[0]) } + + // --- Toggle Views --- + function toggle_view (el, show) { el.classList.toggle('hide', !show) } + function steps_toggle_view (display) { toggle_view(steps_wizard_el, display === 'block') } + + function render_form_component (component_name) { + for (const name in form_input_elements) { + toggle_view(form_input_elements[name], name === component_name) + } + } + + function hide_all_forms () { + for (const name in form_input_elements) { + toggle_view(form_input_elements[name], false) + } + } + + // ------------------------------- + // Protocol: program + // ------------------------------- + + function io_program () { + return function program_protocol (msg) { + const program_handlers = { + load_actions: program_load_actions + } + const { type, data } = msg + const handler = program_handlers[type] || fail + handler(data, type, msg) + } + } + + function program_load_actions (data, type, msg) { + _.up(type, msg.head ? { cause: msg.head } : {}, data) + } + + // ------------------------------- + // Protocol: steps wizard + // ------------------------------- + + function io_steps_wizard () { + return function steps_wizard_protocol (msg) { + const steps_handlers = { + step_clicked: steps_wizard_step_clicked + } + + const { type } = msg + const handler = steps_handlers[type] + if (handler) handler(msg) + else _.up(type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } + + function steps_wizard_step_clicked (msg) { + const { data } = msg + const refs = msg.head ? { cause: msg.head } : {} + _.up('step_clicked', refs, data) + + if (should_execute_step(data)) { + _.up('execute_step', refs, { + action: selected_action, + step: data, + commands: data.commands + }) + } + + function should_execute_step (step_data) { + return step_data && Array.isArray(step_data.commands) && step_data.commands.length > 0 + } + } + + // ------------------------------- + // Protocol: form input + // ------------------------------- + + function io_form_input (component_name) { + return function form_input_protocol (msg) { + const form_input_handlers = { + action_submitted: form_action_submitted, + action_incomplete: form_action_incomplete, + action_complete: form__action_complete + } + const { type, data } = msg + const handler = form_input_handlers[type] || fail + handler(data, type, msg) + } + } + + function form_action_submitted (data, type, msg) { + console.error('action_executor: form_action_submitted', data, 'selected_action:', selected_action) + const step = selected_action.steps[data?.index] + Object.assign(step, { + is_completed: true, + status: 'completed', + data: data.value + }) + console.error('action_executor: step updated', step) + + const refs = msg.head ? { cause: msg.head } : {} + _.program('update_data', refs, all_data) + _.steps_wizard('init_data', refs, selected_action.steps) + + if (selected_action.steps[selected_action.steps.length - 1]?.is_completed) { + _.up('show_submit_btn', refs, null) + } + } + + function form_action_incomplete (data, type, msg) { + console.error('action_executor: form_action_incomplete', data) + const step = selected_action.steps[data?.index] + + if (!step.is_completed) return + + Object.assign(step, { + is_completed: false, + status: 'error', + data: data.value !== undefined ? data.value : undefined + }) + const refs = msg.head ? { cause: msg.head } : {} + _.program('update_data', refs, all_data) + _.steps_wizard('init_data', refs, selected_action.steps) + _.up('hide_submit_btn', refs, null) + } + + function form__action_complete (data, type, msg) { + console.error('action_executor: form__action_complete', data, 'selected_action:', selected_action) + if (!selected_action || !selected_action.steps) { + console.error('action_executor: no selected_action or steps') + return + } + + const all_mandatory_complete = selected_action.steps.every(is_step_complete_or_optional) + console.error('action_executor: all_mandatory_complete:', all_mandatory_complete) + + if (all_mandatory_complete) { + hide_all_forms() + _.up('action_auto_completed', msg.head ? { cause: msg.head } : {}, { selected_action, trigger: 'form' }) + } + + function is_step_complete_or_optional (step) { + return step.is_completed || step.type === 'optional' + } + } + + // ------------------------------- + // onmessage from parent + // ------------------------------- + + function io_up () { + return function onmessage (msg) { + const { type } = msg + if (type === 'docs_toggle') { + _.steps_wizard(type, msg.head ? { cause: msg.head } : {}, msg.data) + for (const name in component_modules) { + _[name](type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } else { + parent_handler[type](msg) + } + } + } + + function update_steps_wizard_for_app (msg) { + const { data } = msg + all_data = data + } + + function load_actions (msg) { + _.program(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function action_submitted (msg) { + const { data } = msg + data.result = JSON.stringify(selected_action.steps.map(step => step.data), null, 2) + + reset_selected_action_steps() + _.program('display_result', msg.head ? { cause: msg.head } : {}, data) + } + + function reset_selected_action_steps () { + if (!selected_action?.steps) return + + selected_action.steps.forEach(step => { + step.data = '' + step.is_completed = false + }) + } + + function render_form (msg) { + const { data } = msg + render_form_component(data.component) + const send = _[data.component] + if (send) { + send('step_data', msg.head ? { cause: msg.head } : {}, data) + } + } + + function parent_selected_action (msg) { selected_action = msg.data } + + function update_data (msg) { + const { data: msg_data, type } = msg + _.program(type, msg.head ? { cause: msg.head } : {}, msg_data) + } + + function activate_steps_wizard (msg) { + if (!all_data) return + const steps_data = all_data.find(matches_selected_action) + selected_action = steps_data + if (!steps_data) return + steps_toggle_view('block') + const data = steps_data.steps + _.steps_wizard('init_data', msg.head ? { cause: msg.head } : {}, data) + + function matches_selected_action (action) { + const target = typeof msg.data === 'string' ? msg.data : msg.data?.name + return action.name === target + } + } + + function form_data (msg) { + // forward init_data to steps_wizard with current action steps + _.steps_wizard('init_data', msg.head ? { cause: msg.head } : {}, msg.data) + } + + function clean_up (msg) { + steps_toggle_view('none') + for (const el of Object.values(form_input_elements)) { + toggle_view(el, false) + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + program: { $: '' }, + steps_wizard: { $: '' }, + net_helper: { $: '' } + } + } + function fallback_instance () { + return { + _: { + program: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs' + } + }, + steps_wizard: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs' + } + }, + 'program>form_input': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>input_test': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>form_tile_split_choice': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + 'program>form_click_rate_test': { + 0: '', + mapping: { + style: 'style', + data: 'data', + docs: 'docs' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'action_executor.css': { + raw: ` + .container { + display: flex; + flex-direction: column; + width: 100%; + } + .program-bar { + display: flex; + } + .form-inputs { + display: flex; + } + .steps-wizard-bar { + display: flex; + } + .hide { + display: none; + } + ` + } + }, + 'variables/': {}, + 'data/': {}, + 'docs/': {} + } + } + } +} diff --git a/src/node_modules/action_executor/package.json b/src/node_modules/action_executor/package.json new file mode 100644 index 0000000..cab1bc3 --- /dev/null +++ b/src/node_modules/action_executor/package.json @@ -0,0 +1 @@ +{"name": "action_executor", "main": "action_executor.js"} diff --git a/src/node_modules/actions/README.md b/src/node_modules/actions/README.md new file mode 100644 index 0000000..7bfa1fc --- /dev/null +++ b/src/node_modules/actions/README.md @@ -0,0 +1 @@ +A menu component that displays a filterable list of available actions with icons, pin states, and default indicators. diff --git a/src/node_modules/actions/actions.js b/src/node_modules/actions/actions.js new file mode 100644 index 0000000..346a2b1 --- /dev/null +++ b/src/node_modules/actions/actions.js @@ -0,0 +1,343 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = actions + +async function actions (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + actions: onactions, + icons: iconject, + hardcons: onhardcons + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const actions_menu = shadow.querySelector('.actions-menu') + + let init = false + let actions = [] + let icons = {} + let hardcons = {} + const docs = DOCS(__filename)(opts.sid) + const on_message = { + filter_actions: handle_filter_actions, + send_selected_action: handle_send_selected_action, + load_actions: handle_load_actions_message, + update_actions_for_app: handle_update_actions_for_app_message + } + const { io, _ } = net(id) + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_filter_actions (msg) { filter(msg.data) } + function handle_send_selected_action (msg) { + _.up('selected_action', msg.head ? { cause: msg.head } : {}, msg.data) + } + function handle_load_actions_message (msg) { handle_load_actions(msg.data) } + function handle_update_actions_for_app_message (msg) { update_actions_for_app(msg.data) } + function onmessage_fail (msg) { fail(msg.data, msg.type) } + function handle_load_actions (data) { + const converted_actions = Object.keys(data).map(convert_action_key) + actions = converted_actions + if (actions.length > 0) register_actions() + create_actions_menu() + + function convert_action_key (action_key) { + return { + name: action_key, + info: 'Run the ' + action_key + ' action.', + icon: 'file', + status: { pinned: false, default: true }, + steps: [] + } + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + create_actions_menu() + init = true + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function iconject (data) { icons = data } + + function onhardcons (data) { + console.log('Hardcons data:', opts.sid, data) + hardcons = { + pin: data[0], + unpin: data[1], + default: data[2], + undefault: data[3] + } + } + + function onactions (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + actions = vars + if (actions.length > 0) register_actions() + create_actions_menu() + } + + function create_actions_menu () { + actions_menu.replaceChildren() + actions.forEach(create_action_item) + } + + function create_action_item (action_data, index) { + const action_item = document.createElement('div') + action_item.classList.add('action-item') + + const this_icon = icons[index] || icons[0] + action_item.innerHTML = ` +
${this_icon}
+
${action_data.name}
+
${action_data.status && action_data.status.pinned ? hardcons.pin : hardcons.unpin}
+
${action_data.status && action_data.status.default ? hardcons.default : hardcons.undefault}
` + on_action_click.info = action_data.info + on_action_click.opts = { state: { name: action_data.name } } + action_item.onclick = docs.wrap_isolated(on_action_click) + actions_menu.appendChild(action_item) + + function on_action_click (event, $) { $($.state.name) } + } + + function register_actions () { + docs.register_actions(actions.map(bind_action)) + + function bind_action (action) { + return { ...action, run: run_action } + + function run_action () { _.up('selected_action', {}, action) } + } + } + + function filter (search_term) { + const items = shadow.querySelectorAll('.action-item') + items.forEach(update_item_visibility) + + function update_item_visibility (item) { + const action_name = item.children[1].textContent.toLowerCase() + const matches = action_name.includes(search_term.toLowerCase()) + item.style.display = matches ? 'flex' : 'none' + } + } + + async function update_actions_for_app (data) { + if (data) { + drive.put('actions/commands.json', data) + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'actions/': { + 'commands.json': { + raw: JSON.stringify([]) + } + }, + 'icons/': { + 'file.svg': { + $ref: 'icon.svg' + }, + 'folder.svg': { + $ref: 'icon.svg' + }, + 'save.svg': { + $ref: 'icon.svg' + }, + 'gear.svg': { + $ref: 'icon.svg' + }, + 'help.svg': { + $ref: 'icon.svg' + }, + 'terminal.svg': { + $ref: 'icon.svg' + }, + 'search.svg': { + $ref: 'icon.svg' + } + }, + 'hardcons/': { + 'pin.svg': { + $ref: 'pin.svg' + }, + 'unpin.svg': { + $ref: 'unpin.svg' + }, + 'default.svg': { + $ref: 'default.svg' + }, + 'undefault.svg': { + $ref: 'undefault.svg' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .actions-container { + position: relative; + background: #202124; + border: 1px solid #3c3c3c; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + height: auto; + max-height: 100%; + min-height: 0; + width: 100%; + box-sizing: border-box; + overflow-y: auto; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #3c3c3c transparent; + color: #e8eaed; + } + + .actions-container::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + .actions-container::-webkit-scrollbar-track { + background: transparent; + } + + .actions-container::-webkit-scrollbar-thumb { + background: #3c3c3c; + border: 2px solid transparent; + border-radius: 999px; + background-clip: content-box; + } + + .actions-container::-webkit-scrollbar-thumb:hover { + background: #5f6368; + border: 2px solid transparent; + background-clip: content-box; + } + + .actions-menu { + padding: 8px 0; + } + + .action-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 16px; + cursor: pointer; + border-bottom: 1px solid #3c3c3c; + transition: background-color 0.2s ease; + } + + .action-item:hover { + background-color: #2d2f31; + } + + .action-item:last-child { + border-bottom: none; + } + + .action-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #a6a6a6; + } + + .action-name { + flex: 1; + font-size: 14px; + color: #e8eaed; + } + + .action-pin .action-default{ + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + font-size: 12px; + opacity: 0.7; + color: #a6a6a6; + } + + svg { + width: 16px; + height: 16px; + } + ` + } + } + } + } + } +} diff --git a/src/node_modules/actions/default.svg b/src/node_modules/actions/default.svg new file mode 100644 index 0000000..d05faac --- /dev/null +++ b/src/node_modules/actions/default.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/actions/icon.svg b/src/node_modules/actions/icon.svg new file mode 100644 index 0000000..e6c11ea --- /dev/null +++ b/src/node_modules/actions/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/node_modules/actions/package.json b/src/node_modules/actions/package.json new file mode 100644 index 0000000..db9362a --- /dev/null +++ b/src/node_modules/actions/package.json @@ -0,0 +1,3 @@ +{ + "main": "actions.js" +} \ No newline at end of file diff --git a/src/node_modules/actions/pin.svg b/src/node_modules/actions/pin.svg new file mode 100644 index 0000000..f1c2599 --- /dev/null +++ b/src/node_modules/actions/pin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/node_modules/actions/undefault.svg b/src/node_modules/actions/undefault.svg new file mode 100644 index 0000000..765ad7f --- /dev/null +++ b/src/node_modules/actions/undefault.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/actions/unpin.svg b/src/node_modules/actions/unpin.svg new file mode 100644 index 0000000..63dd5a0 --- /dev/null +++ b/src/node_modules/actions/unpin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/node_modules/console_history/README.md b/src/node_modules/console_history/README.md new file mode 100644 index 0000000..0573917 --- /dev/null +++ b/src/node_modules/console_history/README.md @@ -0,0 +1 @@ +A component that displays a scrollable history of executed commands with icons, linked items, and action buttons for restore/delete operations. diff --git a/src/node_modules/console_history/commands.json b/src/node_modules/console_history/commands.json new file mode 100644 index 0000000..0150ae2 --- /dev/null +++ b/src/node_modules/console_history/commands.json @@ -0,0 +1,8 @@ +[ + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "Restored" }, + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "copied" }, + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "rename" }, + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "Deleted" }, + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "Linked" }, + { "name_path": "page/header.css - to - light.json", "icon_type": "file", "label": "restore/delete", "pending": true, "restore_data": { "name": "page/header.css" } } +] \ No newline at end of file diff --git a/src/node_modules/console_history/console.svg b/src/node_modules/console_history/console.svg new file mode 100644 index 0000000..709fe2b --- /dev/null +++ b/src/node_modules/console_history/console.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/console_history/console_history.js b/src/node_modules/console_history/console_history.js new file mode 100644 index 0000000..23f692e --- /dev/null +++ b/src/node_modules/console_history/console_history.js @@ -0,0 +1,635 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const DEBUG = require('DEBUG') +const net = require('net_helper') + +module.exports = console_history + +async function console_history (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + commands: oncommands, + icons: iconject + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const commands_list = shadow.querySelector('.commands-list') + const search_input = shadow.querySelector('.console-search-input') + const clear_btn = shadow.querySelector('.console-search-clear') + const filter_btn = shadow.querySelector('.console-search-filter') + search_input.oninput = on_search_input + clear_btn.onclick = on_clear_search + filter_btn.onclick = on_toggle_default_entries + + let default_commands = [] + const live_commands = [] + let search_term = '' + let dricons = [] + let docs_actions = [] + const docs = DOCS(__filename)(opts.sid) + const debug = DEBUG(__filename)(opts.sid) + const { io, _ } = net(id) + debug.on_change(on_debug_change) + + // Register actions with DOCS system + const actions_file = await drive.get('actions/commands.json') + if (actions_file.raw) { + docs_actions = typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw + docs.register_actions(docs_actions) + } else { + console.error('actions.json not found') + } + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + return el + + function io_up () { + const on_message = { + record_closed_tab: handle_record_closed_tab + } + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + function handle_record_closed_tab (msg) { record_closed_tab(msg.data) } + function onmessage_fail (msg) { console.warn('console_history: unknown message', { cause: msg && msg.type }) } + } + + function record_closed_tab (data) { + const tab = data || {} + const entry = { + icon_type: 'file', + name_path: tab.name || tab.id || 'Tab', + label: 'closed', + pending: true, + status: null, + restore_data: { id: tab.id, name: tab.name } + } + live_commands.unshift(entry) + render_commands() + } + + function resolve_closed_tab (entry, status, do_restore) { + entry.pending = false + entry.status = status + entry.label = status + if (do_restore) _.up('restore_tab', {}, entry.restore_data || { name: entry.name_path }) + render_commands() + } + + function on_search_input (e) { + search_term = e.target.value + render_commands() + } + + function on_clear_search () { + search_term = '' + search_input.value = '' + render_commands() + } + + function on_toggle_default_entries () { + debug.set_flag('show_default_entries', !debug.get_flag('show_default_entries')) + } + + function on_debug_change () { render_commands() } + + function create_command_item (command_data, action) { + const command_el = document.createElement('div') + command_el.className = 'command-item' + + const icon_html = dricons[command_data.icon_type] || dricons.file || '' + const is_pending = !!command_data.pending + const status = command_data.status + + let right_html = '' + if (is_pending) { + right_html = + '
' + (dricons.restore || '') + '
' + + '
' + (dricons.delete || '') + '
' + } else if (status) { + const status_class = String(status).toLowerCase().indexOf('delet') === 0 ? 'deleted' : 'restored' + right_html = '' + status + '' + } + + command_el.innerHTML = ` +
+
${icon_html}
+
+
${command_data.name_path}
+
${command_data.label || ''}
+
+ ${right_html ? `
${right_html}
` : ''} +
` + + on_command_click.info = action.info + on_command_click.opts = { state: { action: action.name } } + command_el.onclick = docs.wrap_isolated(on_command_click) + + const restore_el = command_el.querySelector('.restore-action') + const delete_el = command_el.querySelector('.delete-action') + if (restore_el) restore_el.onclick = on_restore_click + if (delete_el) delete_el.onclick = on_delete_click + + function on_restore_click (e) { + e.stopPropagation() + resolve_closed_tab(command_data, 'Restored', true) + } + + function on_delete_click (e) { + e.stopPropagation() + resolve_closed_tab(command_data, 'Deleted', false) + } + + function on_command_click (event, $) { $($.state.action) } + + return command_el + } + function render_commands () { + commands_list.replaceChildren() + const show_defaults = debug.get_flag('show_default_entries') + const base = show_defaults ? live_commands.concat(default_commands) : live_commands.slice() + const term = search_term.trim().toLowerCase() + const visible = term ? base.filter(matches_search) : base + const actions = visible.map(create_command_action) + docs.register_actions(docs_actions.concat(actions)) + visible.forEach(append_command_item) + + function matches_search (command) { + const haystack = [command.name_path, command.label, command.status] + .filter(Boolean) + .join(' ') + .toLowerCase() + return haystack.includes(term) + } + + function create_command_action (command, index) { + const name = 'Select History Entry ' + (index + 1) + return create_action(name, 'Select ' + command.name_path + ' in command history.', select_command) + + function select_command () { + const command_el = commands_list.children[index] + const previous = commands_list.querySelector('.command-item.selected') + if (previous) previous.classList.remove('selected') + command_el.classList.add('selected') + _.up('ui_focus', {}, { type: 'command_history', sid: opts.sid }) + _.up('command_clicked', {}, command) + } + } + + function append_command_item (command, index) { + commands_list.appendChild(create_command_item(command, actions[index])) + } + } + function create_action (name, info, run) { + return { name, info, icon: 'history', status: { hidden: true }, steps: [], run } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + render_commands() + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function oncommands (data) { + const commands_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + default_commands = Array.isArray(commands_data) ? commands_data.map(normalize_default) : [] + } + + function normalize_default (entry) { + return { + icon_type: entry.icon_type || 'file', + name_path: entry.name_path || entry.name || 'entry', + label: entry.label || entry.status || entry.command || '', + pending: !!entry.pending, + status: entry.pending ? null : (entry.status || null), + restore_data: entry.restore_data || { name: entry.name_path } + } + } + + function iconject (data) { + dricons = { + file: data[0] || '', + bulb: data[1] || '', + restore: data[2] || '', + delete: data[3] || '' + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + DEBUG: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + DEBUG: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'commands/': { + 'list.json': { + $ref: 'commands.json' + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'Clear History', + info: 'Clear the stored console history after confirmation.', + icon: 'trash', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Confirm Clear', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Export History', + info: 'Export console history to the selected format and location.', + icon: 'download', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Choose Format', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Select Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Search History', + info: 'Search through recorded console history entries.', + icon: 'search', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Enter Search Term', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'icons/': { + 'file.svg': { + raw: ` + + + ` + }, + 'bulb.svg': { + raw: ` + + + ` + }, + 'restore.svg': { + raw: ` + + + ` + }, + 'delete.svg': { + raw: ` + + ` + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .console-history-container { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + height: 100%; + background: #202124; + border: 1px solid #3c3c3c; + box-sizing: border-box; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + z-index: 1; + overflow: hidden; + color: #e8eaed; + } + + .console-menu { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + padding: 0px; + } + + /* Sticky search bar pinned to the bottom; scrolling the list above + it does not move it. */ + .console-search { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + background: #18191b; + border-top: 1px solid #3c3c3c; + } + + .console-search-icon { + display: flex; + align-items: center; + justify-content: center; + color: #969ba1; + flex: 0 0 auto; + } + + .console-search-input { + flex: 1 1 auto; + min-width: 0; + box-sizing: border-box; + padding: 6px 8px; + background: transparent; + color: #e8eaed; + border: none; + outline: none; + font-size: 13px; + } + + .console-search-input::placeholder { color: #6b7077; } + + .console-search-btn { + display: flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + padding: 0; + background: transparent; + border: none; + color: #969ba1; + cursor: pointer; + border-radius: 4px; + } + + .console-search-btn:hover { + color: #e8eaed; + background: rgba(255, 255, 255, 0.08); + } + + .commands-list { + display: flex; + flex-direction: column; + gap: 0px; + } + + .command-item { + display: flex; + align-items: center; + padding: 8px 14px; + background: transparent; + border-bottom: 1px solid #3c3c3c; + cursor: pointer; + transition: background-color 0.15s ease; + } + + .command-item:last-child { + border-bottom: none; + } + + .command-item:hover { + background: #282a2d; + } + + .command-item.selected { + background: #f56300; + } + + .command-item.selected .command-name, + .command-item.selected .command-label, + .command-item.selected .status-text, + .command-item.selected .action-icon { + color: #fff; + } + + .command-content { + display: flex; + align-items: center; + width: 100%; + gap: 12px; + } + + .command-icon { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 6px; + background: #2b2c2f; + color: #c8ccd2; + flex: 0 0 auto; + } + + .command-item.selected .command-icon { + background: rgba(255, 255, 255, 0.18); + color: #fff; + } + + .command-icon svg { + width: 16px; + height: 16px; + } + + .command-info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1 1 auto; + } + + .command-label { + font-size: 11px; + color: #969ba1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .status-text { + font-size: 12px; + white-space: nowrap; + } + + .status-text.status-restored { color: #46c95d; } + .status-text.status-deleted { color: #ff6b6b; } + + .console-menu::-webkit-scrollbar { + width: 10px; + } + .console-menu::-webkit-scrollbar-track { + background: transparent; + } + .console-menu::-webkit-scrollbar-thumb { + background: #30363d; + border-radius: 999px; + background-clip: content-box; + border: 2px solid transparent; + } + .console-menu::-webkit-scrollbar-thumb:hover { + background: #484f58; + } + .console-menu { + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + } + .command-name { + font-size: 13px; + font-weight: 400; + color: #e8eaed; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .command-path { + font-size: 13px; + color: #969ba1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .command-separator { + color: #969ba1; + margin: 0 4px; + font-size: 13px; + } + + .linked-info { + display: flex; + align-items: center; + gap: 6px; + flex-grow: 1; /* Allow info to take available space */ + + } + + .linked-icon { + display: flex; + align-items: center; + justify-content: center; + width: 16px; + height: 16px; + color: #fbbc04; + } + + .linked-icon svg { + width: 14px; + height: 14px; + } + + .linked-name { + font-size: 13px; + color: #fbbc04; + font-weight: 400; + white-space: nowrap; + } + + .command-actions { + display: flex; + align-items: center; + gap: 10px; /* Adjusted gap */ + margin-left: auto; /* Pushes actions to the right */ + } + + .action-text { + font-size: 13px; + color: #969ba1; + white-space: nowrap; + } + + .action-icon { + display: flex; + align-items: center; + justify-content: center; + width: 20px; + height: 20px; + color: #969ba1; + cursor: pointer; + } + + .action-icon:hover { + color: #e8eaed; + } + + .action-icon svg { + width: 16px; + height: 16px; + } + ` + } + } + } + } + } +} diff --git a/src/node_modules/console_history/package.json b/src/node_modules/console_history/package.json new file mode 100644 index 0000000..96b867f --- /dev/null +++ b/src/node_modules/console_history/package.json @@ -0,0 +1,3 @@ +{ + "main": "console_history.js" +} \ No newline at end of file diff --git a/src/node_modules/docs_window/README.md b/src/node_modules/docs_window/README.md new file mode 100644 index 0000000..a916a82 --- /dev/null +++ b/src/node_modules/docs_window/README.md @@ -0,0 +1 @@ +A component that displays info about other components \ No newline at end of file diff --git a/src/node_modules/docs_window/docs_window.js b/src/node_modules/docs_window/docs_window.js new file mode 100644 index 0000000..cbbadcb --- /dev/null +++ b/src/node_modules/docs_window/docs_window.js @@ -0,0 +1,145 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +module.exports = docs_window + +async function docs_window (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ +
+
No documentation available
+
+
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const close_btn = shadow.querySelector('.close-btn') + const docs_text = shadow.querySelector('.docs-text') + + close_btn.onclick = onclose + + await sdb.watch(onbatch) + + return el + + function onclose () { + _.up('close_docs', {}, null) + } + + function io_up () { + return function onmessage (msg) { + const { type, data } = msg + if (type === 'display_doc') { + display_content(data) + } + } + } + + function display_content (data) { + const content = data.content || undefined + docs_text.textContent = content || 'No documentation available' + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} + +function fallback_module () { + return { + _: { + net_helper: { + $: '' + } + }, + api: fallback_instance + } + + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .docs-window { + position: relative; + background: #1e1e2e; + border: 1px solid #3c3c3c; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); + color: #e8eaed; + overflow: hidden; + display: flex; + justify-content: space-between; + flex-direction: row-reverse; + flex-wrap: nowrap; + align-items: flex-start; + } + .close-btn { + background: transparent; + border: none; + color: #a6a6a6; + cursor: pointer; + font-size: 16px; + padding: 4px 8px; + border-radius: 4px; + transition: background 0.2s, color 0.2s; + } + .close-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: #e8eaed; + } + .docs-content { + padding: 16px; + max-height: 200px; + overflow-y: auto; + } + .docs-text { + font-size: 13px; + line-height: 1.6; + color: #c9d1d9; + margin: 0; + white-space: pre-wrap; + word-wrap: break-word; + } + ` + } + } + } + } + } +} diff --git a/src/node_modules/docs_window/package.json b/src/node_modules/docs_window/package.json new file mode 100644 index 0000000..8328dc5 --- /dev/null +++ b/src/node_modules/docs_window/package.json @@ -0,0 +1 @@ +{"main": "docs_window.js"} diff --git a/src/node_modules/focus_tracker/README.md b/src/node_modules/focus_tracker/README.md new file mode 100644 index 0000000..b617f5c --- /dev/null +++ b/src/node_modules/focus_tracker/README.md @@ -0,0 +1 @@ +A utility component that tracks UI focus changes and broadcasts focused_app_changed messages to the control_unit for coordinating app-specific behaviors. diff --git a/src/node_modules/focus_tracker/focus_tracker.js b/src/node_modules/focus_tracker/focus_tracker.js new file mode 100644 index 0000000..09b8252 --- /dev/null +++ b/src/node_modules/focus_tracker/focus_tracker.js @@ -0,0 +1,91 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +module.exports = focus_tracker + +async function focus_tracker (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + focused + } + const on_message = { + ui_focus: handle_ui_focus + } + // Keep track of the last focused element + let last_focused = null + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_ui_focus (msg) { + if (last_focused !== msg.data.type) { + _.up('focused_app_changed', {}, msg.data) + } + drive.put('focused/current.json', { value: msg.data.type }) + } + + function onmessage_fail (msg) { fail(msg.data, msg.type) } + + await sdb.watch(onbatch) + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function focused (data) { + const tmp = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + last_focused = tmp.value + } +} + +function fallback_module () { + return { + _: { + net_helper: { + $: '' + } + }, + api: fallback_instance + } + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + }, + drive: { + 'focused/': { + 'current.json': { + raw: { value: 'default' } + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/focus_tracker/package.json b/src/node_modules/focus_tracker/package.json new file mode 100644 index 0000000..1474f0b --- /dev/null +++ b/src/node_modules/focus_tracker/package.json @@ -0,0 +1 @@ +{"main": "focus_tracker.js"} diff --git a/src/node_modules/form_click_rate_test/form_click_rate_test.css b/src/node_modules/form_click_rate_test/form_click_rate_test.css new file mode 100644 index 0000000..e5e847a --- /dev/null +++ b/src/node_modules/form_click_rate_test/form_click_rate_test.css @@ -0,0 +1,38 @@ +.click-rate-test { + background: linear-gradient(180deg, #0f1620 0%, #141b24 100%); + border: 1px solid rgba(255,255,255,0.03); + box-shadow: 0 10px 30px rgba(2,6,12,0.6); + color: #e6eef6; + width: 100%; + max-width: 520px; + padding: 14px; + border-radius: 12px; + font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; +} +.click-rate-test .title { font-size:14px; font-weight:700; text-align:center; margin-bottom:10px } +.click-rate-test .click-btn { + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.03); + color: inherit; + padding: 18px; + border-radius: 10px; + display:flex; align-items:center; justify-content:center; gap:8px; + width: 100%; + transition: transform .08s ease, background .08s ease, box-shadow .08s ease; +} +.click-rate-test .click-btn:hover { transform: translateY(-3px); background: rgba(255,255,255,0.03) } +.click-rate-test .click-btn:active { + transform: translateY(0) scale(0.99); + box-shadow: 0 12px 36px rgba(103,195,255,0.16); +} +.click-rate-test .click-btn:disabled { cursor: not-allowed; opacity: 0.45 } +.click-rate-test .count { font-size:22px; font-weight:700 } +.click-rate-test .label { font-size:13px; color: rgba(255,255,255,0.7) } +.click-rate-test .hint, +.click-rate-test .result { + color: rgba(255,255,255,0.5); + font-size:12px; + margin-top:8px; + text-align:center; +} +.click-rate-test .result { color: #67c3ff } diff --git a/src/node_modules/form_click_rate_test/form_click_rate_test.js b/src/node_modules/form_click_rate_test/form_click_rate_test.js new file mode 100644 index 0000000..ac905d2 --- /dev/null +++ b/src/node_modules/form_click_rate_test/form_click_rate_test.js @@ -0,0 +1,199 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_click_rate_test + +async function form_click_rate_test (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + const click_state = { + accessible: true, + step_index: 0, + count: 0, + start: 0, + complete: false + } + const milliseconds_action = { + name: 'Click Rate Result', + info: 'Calculate and submit the milliseconds taken to complete 10 clicks.', + icon: 'timer', + status: { + pinned: false, + default: false + }, + steps: [], + run: run_milliseconds_action + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
Click Rate Test
+ +
Click as fast as you can. The 10th click completes the action.
+
+
+ + ` + const style = shadow.querySelector('style') + const button = shadow.querySelector('.click-btn') + const count_el = shadow.querySelector('.count') + const result_el = shadow.querySelector('.result') + + on_click.info = 'Record one click in the 10-click sequence. This event is not an action until the 10th click.' + on_click.opts = { state: click_state } + button.onclick = docs.wrap_isolated(on_click) + docs.register_actions([milliseconds_action]) + docs.on_docs_mode_change(on_docs_mode_change) + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { + style.replaceChildren(create_style_element()) + + function create_style_element () { + const style_el = document.createElement('style') + style_el.textContent = data[0] + return style_el + } + } + + function ondata (data) { + if (data.length === 0 || !data[0]) return + const result = data[0].result || '' + click_state.count = result ? data[0].click_count || 10 : 0 + click_state.start = 0 + click_state.complete = Boolean(result) + update_count() + show_result(result) + } + + function step_data (data) { + click_state.accessible = data.is_accessible !== false + click_state.step_index = data.index !== undefined ? data.index : 0 + button.disabled = !click_state.accessible + } + + function reset_data () { + reset_clicks() + drive.put('data/form_click_rate_test.json', { + click_count: 0, + result: '' + }) + } + + function on_docs_mode_change (active) { + if (!active) update_count() + } + + function on_click (event, $) { + if (!$.state.accessible || $.state.complete) return + if ($.state.count === 0) $.state.start = Date.now() + + $.state.count += 1 + event.currentTarget.querySelector('.count').textContent = $.state.count + + if ($.state.count === 10) { + $.state.complete = true + $('click_rate_result') + } + } + + async function run_milliseconds_action () { + const result = '10 clicks in ' + (Date.now() - click_state.start) + ' ms' + show_result(result) + await drive.put('data/form_click_rate_test.json', { + click_count: click_state.count, + result + }) + _.up('action_submitted', {}, { value: result, index: click_state.step_index }) + _.up('action_complete', {}, { value: result }) + return result + } + + function reset_clicks () { + click_state.count = 0 + click_state.start = 0 + click_state.complete = false + result_el.textContent = '' + update_count() + } + + function update_count () { count_el.textContent = click_state.count } + function show_result (result) { result_el.textContent = result } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { 0: '' }, + net_helper: { 0: '' } + }, + drive: { + 'style/': { + 'form_click_rate_test.css': { $ref: 'form_click_rate_test.css' } + }, + 'data/': { + 'form_click_rate_test.json': { raw: { click_count: 0, result: '' } } + }, + 'docs/': { 'README.md': { raw: '# Click Rate Test\nClick 10 times as fast as possible.' } } + } + } + } +} diff --git a/src/node_modules/form_click_rate_test/package.json b/src/node_modules/form_click_rate_test/package.json new file mode 100644 index 0000000..0f1a137 --- /dev/null +++ b/src/node_modules/form_click_rate_test/package.json @@ -0,0 +1 @@ +{"main": "form_click_rate_test.js"} diff --git a/src/node_modules/form_input/README.md b/src/node_modules/form_input/README.md new file mode 100644 index 0000000..3639e66 --- /dev/null +++ b/src/node_modules/form_input/README.md @@ -0,0 +1 @@ +A text input component used in multi-step wizards that validates input length and emits action_submitted/action_incomplete events. diff --git a/src/node_modules/form_input/form_input.js b/src/node_modules/form_input/form_input.js new file mode 100644 index 0000000..b38d867 --- /dev/null +++ b/src/node_modules/form_input/form_input.js @@ -0,0 +1,202 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_input +async function form_input (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + let input_accessible = true + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+ +
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const input_field_el = shadow.querySelector('.input-field') + const overlay_el = shadow.querySelector('.overlay-lock') + + input_field_el.oninput = on_input_field_input + + async function on_input_field_input () { + if (!input_accessible) return + await drive.put('data/form_input.json', { + input_field: input_field_el.value + }) + if (input_field_el.value.length >= 10) { + _.up('action_submitted', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + console.log('mark_as_complete') + } else { + _.up('action_incomplete', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + } + } + + await sdb.watch(onbatch) + const parent_handler = { + step_data, + reset_data + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + + function ondata (data) { + if (data.length > 0) { + const input_data = data[0] + if (input_data.input_field) { + input_field_el.value = input_data.input_field + } + } else { + input_field_el.value = '' + } + } + + function io_up () { + return function onmessage ({ type, data }) { + console.log('message from form_input', type, data) + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + function step_data (data, type) { + current_step = data + input_field_el.value = data?.data + + input_accessible = data.is_accessible !== false + + overlay_el.hidden = input_accessible + + input_field_el.placeholder = input_accessible + ? 'Type to submit' + : 'Input disabled for this step' + } + + function reset_data (data, type) { + input_field_el.value = '' + drive.put('data/form_input.json', { + input_field: '' + }) + } +} +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .input-display { + background: #131315; + border-radius: 16px; + border: 1px solid #3c3c3c; + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + position: relative; + } + .input-display:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .overlay-lock { + position: absolute; + inset: 0; + background: transparent; + z-index: 10; + cursor: not-allowed; + }` + } + }, + 'data/': { + 'form_input.json': { + raw: { + input_field: '' + } + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/form_input/package.json b/src/node_modules/form_input/package.json new file mode 100644 index 0000000..8fb8332 --- /dev/null +++ b/src/node_modules/form_input/package.json @@ -0,0 +1 @@ +{"main": "form_input.js"} diff --git a/src/node_modules/form_tile_split_choice/README.md b/src/node_modules/form_tile_split_choice/README.md new file mode 100644 index 0000000..a347d78 --- /dev/null +++ b/src/node_modules/form_tile_split_choice/README.md @@ -0,0 +1 @@ +A Tile Split Choice component used in multi-step wizards that validates choice and emits action_submitted/action_incomplete events. diff --git a/src/node_modules/form_tile_split_choice/form_tile_split_choice.css b/src/node_modules/form_tile_split_choice/form_tile_split_choice.css new file mode 100644 index 0000000..a927856 --- /dev/null +++ b/src/node_modules/form_tile_split_choice/form_tile_split_choice.css @@ -0,0 +1,34 @@ +.tile-split-chooser { + background: linear-gradient(180deg, #0f1620 0%, #141b24 100%); + border: 1px solid rgba(255,255,255,0.03); + box-shadow: 0 10px 30px rgba(2,6,12,0.6); + color: #e6eef6; + width: 100%; + max-width: 520px; + padding: 14px; + border-radius: 12px; + font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; +} +.tile-split-chooser .title { font-size:14px; font-weight:700; text-align:center; margin-bottom:10px } +.tile-split-chooser .choices { display:flex; gap:10px; justify-content:space-between } +.tile-split-chooser .choice-btn { + background: rgba(255,255,255,0.02); + border: 1px solid rgba(255,255,255,0.03); + color: inherit; + padding: 12px 18px; + border-radius: 10px; + display:flex; align-items:center; justify-content:center; gap:10px; + transition: transform .12s ease, background .12s ease, box-shadow .12s ease; + flex: 1 1 0; + min-width: 0; +} +.tile-split-chooser .choice-btn:hover { transform: translateY(-3px); background: rgba(255,255,255,0.03) } +.tile-split-chooser .choice-btn.active { + box-shadow: 0 12px 36px rgba(103,195,255,0.16); + border-color: rgba(103,195,255,0.36); + background: linear-gradient(180deg, rgba(103,195,255,0.06), rgba(103,195,255,0.02)); + transform: translateY(-4px) scale(1.02); +} +.tile-split-chooser .arrow { opacity:0.95 } +.tile-split-chooser .label { font-size:13px } +.tile-split-chooser .hint { color: rgba(255,255,255,0.5); font-size:12px; margin-top:8px; text-align:center } diff --git a/src/node_modules/form_tile_split_choice/form_tile_split_choice.js b/src/node_modules/form_tile_split_choice/form_tile_split_choice.js new file mode 100644 index 0000000..4a17853 --- /dev/null +++ b/src/node_modules/form_tile_split_choice/form_tile_split_choice.js @@ -0,0 +1,169 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = form_tile_split_choice +async function form_tile_split_choice (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
Split Tile
+
+ + + + +
+
Choose direction to split the tile
+
+ + ` + const style = shadow.querySelector('style') + const buttons = Array.from(shadow.querySelectorAll('.choice-btn')) + + buttons.forEach(btn => btn.addEventListener('click', on_choice_click)) + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { + style.replaceChildren(create_style_element()) + + function create_style_element () { + const style_el = document.createElement('style') + style_el.textContent = data[0] + return style_el + } + } + + function ondata (data) { + // support persisted/default choice if present + if (data.length > 0 && data[0] && data[0].choice) { + highlight_choice(String(data[0].choice)) + } + } + + function step_data (data) { + current_step = data + } + + function reset_data () { + // nothing for now + } + + async function on_choice_click (ev) { + const choice = ev.currentTarget.getAttribute('data-choice') + await drive.put('data/form_tile_split_choice.json', { choice }) + highlight_choice(choice) + _.up('action_submitted', {}, { value: choice, index: current_step?.index ?? 0 }) + + // If this is a single-step action, auto-complete the action + if (current_step && current_step.total_steps === 1) { + _.up('action_complete', {}, { value: choice }) + } + } + + function highlight_choice (choice) { + buttons.forEach(b => { + const isActive = b.getAttribute('data-choice') === choice + b.classList.toggle('active', isActive) + b.setAttribute('aria-pressed', isActive ? 'true' : 'false') + if (isActive) { + b.style.background = 'linear-gradient(180deg, rgba(103,195,255,0.06), rgba(103,195,255,0.02))' + b.style.boxShadow = '0 12px 36px rgba(103,195,255,0.16)' + b.style.borderColor = 'rgba(103,195,255,0.36)' + b.style.transform = 'translateY(-2px) scale(1.01)' + } else { + b.style.background = '' + b.style.boxShadow = '' + b.style.borderColor = '' + b.style.transform = '' + } + }) + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { $: '' } + // DOCS: { $: '' }, + } + } + + function fallback_instance () { + return { + _: { + net_helper: { 0: '' } + // DOCS: { 0: '' }, + }, + drive: { + 'style/': { + 'form_tile_split_choice.css': { $ref: 'form_tile_split_choice.css' } + }, + 'data/': { + 'form_tile_split_choice.json': { raw: { choice: null } } + }, + 'docs/': { 'README.md': { $ref: 'README.md' } } + } + } + } +} diff --git a/src/node_modules/form_tile_split_choice/package.json b/src/node_modules/form_tile_split_choice/package.json new file mode 100644 index 0000000..db2cf1a --- /dev/null +++ b/src/node_modules/form_tile_split_choice/package.json @@ -0,0 +1 @@ +{"main": "form_tile_split_choice.js"} diff --git a/src/node_modules/graph_viewer/README.md b/src/node_modules/graph_viewer/README.md new file mode 100644 index 0000000..32527a3 --- /dev/null +++ b/src/node_modules/graph_viewer/README.md @@ -0,0 +1 @@ +A wrapper component that initializes and manages the graph-explorer visualization with a local graphdb instance for displaying hierarchical data. diff --git a/src/node_modules/graph_viewer/entries.json b/src/node_modules/graph_viewer/entries.json new file mode 100644 index 0000000..26ea45a --- /dev/null +++ b/src/node_modules/graph_viewer/entries.json @@ -0,0 +1,770 @@ +{ + "/": { + "name": "root", + "type": "root", + "subs": [ + "/pins", + "/code", + "/data", + "/tasks" + ], + "hubs": [ + null + ] + }, + "/pins": { + "name": "pins", + "type": "folder", + "subs": ["/data/themes"], + "hubs": [ + "/" + ] + }, + "/code": { + "name": "code", + "type": "folder", + "subs": [ + "/code/playproject_website", + "/code/theme_widget", + "/code/text_editor" + ], + "hubs": [ + "/" + ] + }, + "/data": { + "name": "data", + "type": "folder", + "subs": [ + "/data/themes" + ], + "hubs": [ + "/" + ] + }, + "/tasks": { + "name": "tasks", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget" + ], + "hubs": [ + "/" + ] + }, + "/code/playproject_website": { + "name": "playproject_website", + "type": "folder", + "subs": [ + "/code/playproject_website/index.html", + "/code/playproject_website/main.js", + "/code/playproject_website/styles.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/playproject_website/index.html": { + "name": "index.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/playproject_website/main.js": { + "name": "main.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/playproject_website/styles.css": { + "name": "styles.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/playproject_website" + ] + }, + "/code/theme_widget": { + "name": "theme_widget", + "type": "folder", + "subs": [ + "/code/theme_widget/widget.html", + "/code/theme_widget/widget.js", + "/code/theme_widget/theme.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/theme_widget/widget.html": { + "name": "widget.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/theme_widget/widget.js": { + "name": "widget.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/theme_widget/theme.css": { + "name": "theme.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/theme_widget" + ] + }, + "/code/text_editor": { + "name": "text_editor", + "type": "folder", + "subs": [ + "/code/text_editor/editor.html", + "/code/text_editor/editor.js", + "/code/text_editor/editor.css" + ], + "hubs": [ + "/code" + ] + }, + "/code/text_editor/editor.html": { + "name": "editor.html", + "type": "html-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/code/text_editor/editor.js": { + "name": "editor.js", + "type": "js-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/code/text_editor/editor.css": { + "name": "editor.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/code/text_editor" + ] + }, + "/data/themes": { + "name": "themes", + "type": "folder", + "subs": [ + "/data/themes/fantasy.json", + "/data/themes/electro.json", + "/data/themes/light.json", + "/data/themes/night.json" + ], + "hubs": [ + "/data" + ] + }, + "/data/themes/fantasy.json": { + "name": "fantasy.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/electro.json": { + "name": "electro.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/light.json": { + "name": "light.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/night.json": { + "name": "night.json", + "type": "json-file", + "subs": [ + "/data/themes/night.json/page:css", + "/data/themes/night.json/page/header:css", + "/data/themes/night.json/page/header/menu:css", + "/data/themes/night.json/page/projects:css", + "/data/themes/night.json/page/footer:css", + "/data/themes/night.json/page/footer/socials:css" + ], + "hubs": [ + "/data/themes" + ] + }, + "/data/themes/night.json/page:css": { + "name": "page:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/header:css": { + "name": "page/header:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/header/menu:css": { + "name": "page/header/menu:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/header:css" + ] + }, + "/data/themes/night.json/page/projects:css": { + "name": "page/projects:css", + "type": "folder", + "subs": [ + "/data/themes/night.json/page/projects:css/header.css", + "/data/themes/night.json/page/projects:css/1.css" + ], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/projects:css/header.css": { + "name": "header.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/projects:css" + ] + }, + "/data/themes/night.json/page/projects:css/1.css": { + "name": "1.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/projects:css" + ] + }, + "/data/themes/night.json/page/footer:css": { + "name": "page/footer:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json" + ] + }, + "/data/themes/night.json/page/footer/socials:css": { + "name": "page/footer/socials:css", + "type": "file", + "subs": [], + "hubs": [ + "/data/themes/night.json/page/footer:css" + ] + }, + "/tasks/0:theme_widget": { + "name": "0:theme_widget", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/task.json", + "/tasks/0:theme_widget/state", + "/tasks/0:theme_widget/theme-widget", + "/tasks/0:theme_widget/subs" + ], + "hubs": [ + "/tasks" + ] + }, + "/tasks/0:theme_widget/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/state": { + "name": "state", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/state/session.autosave", + "/tasks/0:theme_widget/state/undo.history" + ], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/state/session.autosave": { + "name": "session.autosave", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/state" + ] + }, + "/tasks/0:theme_widget/state/undo.history": { + "name": "undo.history", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/state" + ] + }, + "/tasks/0:theme_widget/theme-widget": { + "name": "theme-widget", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/subs": { + "name": "subs", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject", + "/tasks/0:theme_widget/subs/2:text_editor", + "/tasks/0:theme_widget/subs/3:text_editor" + ], + "hubs": [ + "/tasks/0:theme_widget" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject": { + "name": "1:playproject", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/task.json", + "/tasks/0:theme_widget/subs/1:playproject/state", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/state": { + "name": "state", + "type": "folder", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io": { + "name": "playproject-io", + "type": "file", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/topnav", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/header", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/supporters", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/footer" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject", + "/data/themes/night.json" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/topnav": { + "name": "topnav", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/header": { + "name": "header", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects": { + "name": "projects", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/projects", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/datdot", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/played", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/smartcontract_codes", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/wizardamigos", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/dat_ecosystem", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/data_shell" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/projects": { + "name": "projects", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css": { + "name": "css", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/light:page:css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/header.css", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/1.css" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/light:page:css": { + "name": "light:page:css", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/header.css": { + "name": "header.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css/1.css": { + "name": "1.css", + "type": "css-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/css" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/datdot": { + "name": "datdot", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/played": { + "name": "played", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/smartcontract_codes": { + "name": "smartcontract_codes", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/wizardamigos": { + "name": "wizardamigos", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/dat_ecosystem": { + "name": "dat_ecosystem", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects/data_shell": { + "name": "data_shell", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/projects" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/supporters": { + "name": "supporters", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors": { + "name": "our_contributors", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nina", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jam", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mauve", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Fiona", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Toshi", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ailin", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Kayla", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Tommings", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Santies", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Pepe", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jannis", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nora", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mimi", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Helenphina", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ali", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ibrar", + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Cypher" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nina": { + "name": "Nina", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jam": { + "name": "Jam", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mauve": { + "name": "Mauve", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Fiona": { + "name": "Fiona", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Toshi": { + "name": "Toshi", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ailin": { + "name": "Ailin", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Kayla": { + "name": "Kayla", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Tommings": { + "name": "Tommings", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Santies": { + "name": "Santies", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Pepe": { + "name": "Pepe", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Jannis": { + "name": "Jannis", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Nora": { + "name": "Nora", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Mimi": { + "name": "Mimi", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Helenphina": { + "name": "Helenphina", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ali": { + "name": "Ali", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Ibrar": { + "name": "Ibrar", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors/Cypher": { + "name": "Cypher", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/our_contributors" + ] + }, + "/tasks/0:theme_widget/subs/1:playproject/playproject-io/footer": { + "name": "footer", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/1:playproject/playproject-io" + ] + }, + "/tasks/0:theme_widget/subs/2:text_editor": { + "name": "2:text_editor", + "type": "folder", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor": { + "name": "3:text_editor", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/task.json", + "/tasks/0:theme_widget/subs/3:text_editor/state", + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ], + "hubs": [ + "/tasks/0:theme_widget/subs" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/task.json": { + "name": "task.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/state": { + "name": "state", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/state/night.json" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/state/night.json": { + "name": "night.json", + "type": "json-file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/state" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor": { + "name": "editor", + "type": "folder", + "subs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor/tetxtarea", + "/tasks/0:theme_widget/subs/3:text_editor/editor/toolbar" + ], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor/tetxtarea": { + "name": "tetxtarea", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ] + }, + "/tasks/0:theme_widget/subs/3:text_editor/editor/toolbar": { + "name": "toolbar", + "type": "file", + "subs": [], + "hubs": [ + "/tasks/0:theme_widget/subs/3:text_editor/editor" + ] + } +} diff --git a/src/node_modules/graph_viewer/graph_viewer.js b/src/node_modules/graph_viewer/graph_viewer.js new file mode 100644 index 0000000..b945252 --- /dev/null +++ b/src/node_modules/graph_viewer/graph_viewer.js @@ -0,0 +1,277 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') +const graph_explorer = require('graph-explorer') +const graphdb = require('./graphdb') + +module.exports = graph_viewer + +async function graph_viewer (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + let db = null + let latest_entries = null + let graph_explorer_connected = false + + // Protocol + const { io, _ } = net(id) + io.on = { + up: io_up(), + graph_explorer: io_graph_explorer() + } + if (invite) io.accept(invite) + + const on = { + theme: inject, + entries: on_entries + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const subs = await sdb.watch(onbatch) + + const explorer_el = await graph_explorer(subs[0], io.invite('graph_explorer', { up: id })) + graph_explorer_connected = true + if (latest_entries) _.graph_explorer('db_initialized', {}, { entries: latest_entries }) + shadow.append(explorer_el) + + return el + + function io_up () { + const parent_handlers = { + execute_step: parent_execute_step, + set_mode: parent_forward_to_graph_explorer, + set_search_query: parent_forward_to_graph_explorer, + select_nodes: parent_forward_to_graph_explorer, + expand_node: parent_forward_to_graph_explorer, + collapse_node: parent_forward_to_graph_explorer, + toggle_node: parent_forward_to_graph_explorer, + get_selected: parent_forward_to_graph_explorer, + get_confirmed: parent_forward_to_graph_explorer, + clear_selection: parent_forward_to_graph_explorer, + set_flag: parent_forward_to_graph_explorer, + scroll_to_node: parent_forward_to_graph_explorer, + docs_toggle: parent_forward_to_graph_explorer + } + return function onmessage (msg) { + const handler = parent_handlers[msg.type] || fail + handler(msg) + } + } + + function parent_execute_step (msg) { + const commands = get_step_commands(msg.data) + for (const command of commands) { + const refs = msg.head ? { cause: msg.head } : {} + const data = command.data !== undefined ? command.data : {} + _.graph_explorer(command.type, refs, data) + } + } + + function parent_forward_to_graph_explorer (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.graph_explorer(msg.type, refs, msg.data) + } + + function get_step_commands (data) { + if (!data) return [] + if (Array.isArray(data.commands)) return data.commands.filter(has_command_type) + if (data.command && has_command_type(data.command)) return [data.command] + if (has_command_type(data)) { + return [{ type: data.type, data: data.data !== undefined ? data.data : {} }] + } + return [] + + function has_command_type (command) { + return command && typeof command.type === 'string' && command.type.length > 0 + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const handler = on[type] || fail + handler({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + function inject ({ data }) { sheet.replaceSync(data.join('\n')) } + + function on_entries ({ data }) { + if (!data || !data[0]) { + console.error('Entries data is missing or empty.') + latest_entries = {} + db = graphdb({}) + if (graph_explorer_connected) _.graph_explorer('db_initialized', {}, { entries: {} }) + return + } + + let parsed_data + try { + parsed_data = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + } catch (e) { + console.error('Failed to parse entries data:', e) + parsed_data = {} + } + + if (typeof parsed_data !== 'object' || !parsed_data) { + console.error('Parsed entries data is not a valid object.') + parsed_data = {} + } + + db = graphdb(parsed_data) + latest_entries = parsed_data + if (graph_explorer_connected) _.graph_explorer('db_initialized', {}, { entries: parsed_data }) + } + + // --------------------------------------------------------- + // PROTOCOL + // --------------------------------------------------------- + + function io_graph_explorer () { + return function graph_explorer_protocol(msg) { + const { type } = msg + const db_handler = { + db_get: params => db.get(params.path), + db_has: params => db.has(params.path), + db_is_empty: () => db.is_empty(), + db_root: () => db.root(), + db_keys: () => db.keys(), + db_raw: () => db.raw() + } + + if (type.startsWith('db_')) { + handle_db_request(msg) + } else { + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function handle_db_request ({ head, type, data }) { + const handler = db ? db_handler[type] || db_fail : db_fail + _.graph_explorer('db_response', { cause: head }, { result: handler(data) }) + + function db_fail () { + const msg = db ? '[graph_viewer] Unknown db operation:' : '[graph_viewer] Database not initialized yet' + console.warn(msg, type) + return null + } + } + } + } +} + +function fallback_module () { + return { + _: { + 'graph-explorer': { + $: '' + }, + './graphdb': { + $: '' + }, + net_helper: { + $: '' + } + }, + api: fallback_instance + } + + function fallback_instance () { + return { + _: { + 'graph-explorer': { + $: '', + 0: '', + mapping: { + style: 'theme', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + }, + './graphdb': { + $: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'theme/': { + 'style.css': { + raw: ` + :host { + display: block; + height: 100%; + width: 100%; + } + ` + } + }, + 'entries/': { + 'entries.json': { + $ref: 'entries.json' + } + }, + 'runtime/': { + 'node_height.json': { raw: '16' }, + 'vertical_scroll_value.json': { raw: '0' }, + 'horizontal_scroll_value.json': { raw: '0' }, + 'selected_instance_paths.json': { raw: '[]' }, + 'confirmed_selected.json': { raw: '[]' }, + 'instance_states.json': { raw: '{}' }, + 'search_entry_states.json': { raw: '{}' }, + 'last_clicked_node.json': { raw: 'null' }, + 'view_order_tracking.json': { raw: '{}' } + }, + 'mode/': { + 'current_mode.json': { raw: '"menubar"' }, + 'previous_mode.json': { raw: '"menubar"' }, + 'search_query.json': { raw: '""' }, + 'multi_select_enabled.json': { raw: 'false' }, + 'select_between_enabled.json': { raw: 'false' } + }, + 'flags/': { + 'hubs.json': { raw: '"default"' }, + 'selection.json': { raw: 'true' }, + 'recursive_collapse.json': { raw: 'true' } + }, + 'keybinds/': { + 'navigation.json': { + raw: JSON.stringify({ + ArrowUp: 'navigate_up_current_node', + ArrowDown: 'navigate_down_current_node', + 'Control+ArrowDown': 'toggle_subs_for_current_node', + 'Control+ArrowUp': 'toggle_hubs_for_current_node', + 'Alt+s': 'multiselect_current_node', + 'Alt+b': 'select_between_current_node', + 'Control+m': 'toggle_search_mode', + 'Alt+j': 'jump_to_next_duplicate' + }) + } + }, + 'undo/': { + 'stack.json': { raw: '[]' } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/graph_viewer/graphdb.js b/src/node_modules/graph_viewer/graphdb.js new file mode 100644 index 0000000..c094685 --- /dev/null +++ b/src/node_modules/graph_viewer/graphdb.js @@ -0,0 +1,27 @@ +module.exports = graphdb + +function graphdb (entries) { + // Validate entries + if (!entries || typeof entries !== 'object') { + console.warn('[graphdb] Invalid entries provided, using empty object') + entries = {} + } + + const api = { + get, + has, + keys, + is_empty, + root, + raw + } + + return api + + function get (path) { return entries[path] || null } + function has (path) { return path in entries } + function keys () { return Object.keys(entries) } + function is_empty () { return Object.keys(entries).length === 0 } + function root () { return entries['/'] || null } + function raw () { return entries } +} diff --git a/src/node_modules/graph_viewer/package.json b/src/node_modules/graph_viewer/package.json new file mode 100644 index 0000000..b64d886 --- /dev/null +++ b/src/node_modules/graph_viewer/package.json @@ -0,0 +1 @@ +{"main": "graph_viewer.js"} diff --git a/src/node_modules/helpers/helpers.js b/src/node_modules/helpers/helpers.js new file mode 100644 index 0000000..01f0669 --- /dev/null +++ b/src/node_modules/helpers/helpers.js @@ -0,0 +1,24 @@ +module.exports = { resource } + +function resource (timeout = 1000) { + const states = {} + return { set, get } + function load (pid) { return states[pid] || (states[pid] = { item: null, pending: [] }) } + function set (pid, item) { + const state = load(pid) + state.item = item + const { pending } = state + state.pending = [] + pending.map(resolve_pending_waiter) + + function resolve_pending_waiter (waiter) { waiter.resolve(item) } + } + function get (pid) { + return new Promise(on) + function on (resolve, reject) { + const { item, pending } = load(pid) + if (item) return resolve(item) + pending.push({ resolve, reject }) + } + } +} diff --git a/src/node_modules/helpers/package.json b/src/node_modules/helpers/package.json new file mode 100644 index 0000000..cf61eb4 --- /dev/null +++ b/src/node_modules/helpers/package.json @@ -0,0 +1 @@ +{"main": "helpers.js"} diff --git a/src/node_modules/input_test/README.md b/src/node_modules/input_test/README.md new file mode 100644 index 0000000..06d9447 --- /dev/null +++ b/src/node_modules/input_test/README.md @@ -0,0 +1 @@ +A test input component similar to form_input used for demonstrating and testing the second type of form input in multi-step workflows. diff --git a/src/node_modules/input_test/input_test.js b/src/node_modules/input_test/input_test.js new file mode 100644 index 0000000..edbd043 --- /dev/null +++ b/src/node_modules/input_test/input_test.js @@ -0,0 +1,211 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = input_test +async function input_test (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + data: ondata + } + + let current_step = null + let input_accessible = true + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
Testing 2nd Type
+
+ + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const input_field_el = shadow.querySelector('.input-field') + const overlay_el = shadow.querySelector('.overlay-lock') + + input_field_el.oninput = on_input_field_input + + async function on_input_field_input () { + if (!input_accessible) return + + await drive.put('data/input_test.json', { + input_field: input_field_el.value + }) + + if (input_field_el.value.length >= 10) { + _.up('action_submitted', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + console.log('mark_as_complete') + } else { + _.up('action_incomplete', {}, { + value: input_field_el.value, + index: current_step.index !== undefined ? current_step.index : 0 + }) + } + } + + await sdb.watch(onbatch) + + const parent_handler = { + step_data, + reset_data + } + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + + function ondata (data) { + if (data.length > 0) { + const input_data = data[0] + if (input_data.input_field) { + input_field_el.value = input_data.input_field + } + } else { + input_field_el.value = '' + } + } + + // ------------------ + // Parent Observer + // ------------------ + + function io_up () { + return function onmessage ({ type, data }) { + console.log('message from input_test', type, data) + const handler = parent_handler[type] || fail + handler(data, type) + } + } + + function step_data (data, type) { + current_step = data + + input_accessible = data.is_accessible !== false + + overlay_el.hidden = input_accessible + + input_field_el.placeholder = input_accessible + ? 'Type to submit' + : 'Input disabled for this step' + } + + function reset_data (data, type) { + input_field_el.value = '' + drive.put('data/input_test.json', { + input_field: '' + }) + } +} +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .title { + color: #e8eaed; + font-size: 18px; + display: flex; + align-items: center; + } + .input-display { + position: relative; + background: #131315; + border-radius: 16px; + border: 1px solid #3c3c3c; + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + } + .input-display:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .overlay-lock { + position: absolute; + inset: 0; + background: transparent; + z-index: 10; + cursor: not-allowed; + }` + } + }, + 'data/': { + 'input_test.json': { + raw: { + input_field: '' + } + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/input_test/package.json b/src/node_modules/input_test/package.json new file mode 100644 index 0000000..18dfc53 --- /dev/null +++ b/src/node_modules/input_test/package.json @@ -0,0 +1 @@ +{"main": "input_test.js"} diff --git a/src/node_modules/menu/menu.js b/src/node_modules/menu/menu.js new file mode 100644 index 0000000..d641ad9 --- /dev/null +++ b/src/node_modules/menu/menu.js @@ -0,0 +1,278 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) + +module.exports = create_component_menu +async function create_component_menu (opts, names, inicheck, callbacks) { + const { sdb } = await get(opts.sid) + const { drive } = sdb + const on = { + style: inject + } + const { + on_checkbox_change, + on_label_click, + on_select_all_toggle, + on_resize_toggle + } = callbacks + + const checkobject = {} + inicheck.forEach(mark_checked_index) + + function mark_checked_index (checked_position) { checkobject[checked_position - 1] = true } + + const all_checked = inicheck.length === 0 || Object.keys(checkobject).length === names.length + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` + ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const menu = shadow.querySelector('.menu') + const toggle_btn = shadow.querySelector('.menu-toggle-button') + const unselect_btn = shadow.querySelector('.unselect-all-button') + const resize_btn = shadow.querySelector('.resize-toggle-button') + const list = shadow.querySelector('.menu-list') + + names.forEach(create_menu_item) + + function create_menu_item (name, index) { + const is_checked = all_checked || checkobject[index] === true + const menu_item = document.createElement('li') + menu_item.className = 'menu-item' + menu_item.innerHTML = ` + ${name} + + ` + list.appendChild(menu_item) + + const checkbox = menu_item.querySelector('input') + const label = menu_item.querySelector('span') + + checkbox.onchange = on_checkbox_change_event + label.onclick = on_label_click_event + + function on_checkbox_change_event (e) { on_checkbox_change({ index, checked: e.target.checked }) } + function on_label_click_event () { + on_label_click({ index, name }) + menu.classList.add('hidden') + } + } + await sdb.watch(onbatch) + // event listeners + console.log('resize_btn', resize_btn) + toggle_btn.onclick = on_toggle_btn + unselect_btn.onclick = on_unselect_btn + resize_btn.onclick = on_resize_btn + document.onclick = handle_document_click + + return el + + function on_toggle_btn (e) { + e.stopPropagation() + menu.classList.toggle('hidden') + } + + function on_unselect_btn () { + const select_all = unselect_btn.textContent === 'Select All' + unselect_btn.textContent = select_all ? 'Unselect All' : 'Select All' + list.querySelectorAll('input[type="checkbox"]').forEach(update_checkbox_state) + on_select_all_toggle({ selectAll: select_all }) + + function update_checkbox_state (checkbox) { checkbox.checked = select_all } + } + + function on_resize_btn () { + console.log('on_resize_btn') + on_resize_toggle() + } + + function handle_document_click (e) { + const path = e.composedPath() + if (!menu.classList.contains('hidden') && !path.includes(el)) { + menu.classList.add('hidden') + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} +function fallback_module () { + return { + api: fallback_instance + } + function fallback_instance () { + return { + drive: { + 'style/': { + 'theme.css': { + raw: ` + :host { + display: block; + position: sticky; + top: 0; + z-index: 100; + background-color: #e0e0e0; + } + + .nav-bar-container-inner { + } + + .nav-bar { + display: flex; + position: relative; + justify-content: center; + align-items: center; + padding: 10px 20px; + border-bottom: 2px solid #333; + min-height: 30px; + } + + .menu-toggle-button { + padding: 10px; + background-color: #e0e0e0; + border: none; + cursor: pointer; + border-radius: 5px; + font-weight: bold; + } + + .menu-toggle-button:hover { + background-color: #d0d0d0; + } + + .menu.hidden { + display: none; + } + + .menu { + display: block; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + width: 250px; + max-width: 90%; + background-color: #f0f0f0; + padding: 10px; + border-radius: 0 0 5px 5px; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + z-index: 101; + } + + .menu-header { + margin-bottom: 10px; + text-align: center; + } + + .unselect-all-button { + padding: 8px 12px; + border: none; + background-color: #d0d0d0; + cursor: pointer; + border-radius: 5px; + width: 100%; + margin-bottom: 5px; + } + + .unselect-all-button:hover { + background-color: #c0c0c0; + } + + .resize-toggle-button { + padding: 8px 12px; + border: none; + background-color: #d0d0d0; + cursor: pointer; + border-radius: 5px; + width: 100%; + } + + .resize-toggle-button:hover { + background-color: #c0c0c0; + } + + .menu-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 400px; + overflow-y: auto; + background-color: #f0f0f0; + } + + .menu-list::-webkit-scrollbar { + width: 8px; + } + + .menu-list::-webkit-scrollbar-track { + background: #f0f0f0; + } + + .menu-list::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 4px; + } + + .menu-list::-webkit-scrollbar-thumb:hover { + background: #bbb; + } + + .menu-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px 5px; + border-bottom: 1px solid #ccc; + } + + .menu-item span { + cursor: pointer; + flex-grow: 1; + margin-right: 10px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .menu-item span:hover { + color: #007bff; + } + + .menu-item:last-child { + border-bottom: none; + } + + .menu-item input[type="checkbox"] { + flex-shrink: 0; + }` + } + } + } + } + } +} diff --git a/src/node_modules/menu/package.json b/src/node_modules/menu/package.json new file mode 100644 index 0000000..6b0f86b --- /dev/null +++ b/src/node_modules/menu/package.json @@ -0,0 +1 @@ +{"main": "menu.js"} diff --git a/src/node_modules/net_helper/README.md b/src/node_modules/net_helper/README.md new file mode 100644 index 0000000..00465cf --- /dev/null +++ b/src/node_modules/net_helper/README.md @@ -0,0 +1,189 @@ +# net_helper + +`net_helper` provides a shared message router for component-to-component communication. + +## API + +```js +const net = require('net_helper') + +const { io, _ } = net(id) +io.on = { + up: io_up(), + petname: io_petname() +} +if (invite) io.accept(invite) +``` + +`net(id)` returns: + +- `io.invite(name, ids)` +- `io.accept(invite)` +- `io.on` +- `_` + +## Invite / Accept Flow + +A parent creates an invite for a child: + +```js +const child = await dependency(subs[0], io.invite('petname', { up: id })) +``` + +The child accepts that invite: + +```js +async function dependency (opts, invite) { + const { id } = await get(opts.sid) + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) +} +``` + +Flow: + +- Parent registers handlers on `io.on` +- Parent passes `io.invite(name, ids)` to the child +- Child calls `io.accept(invite)` +- `net_helper` wires both directions and creates `_` channel helpers automatically +- Components send by channel name, not by constructing `message.head` manually + +## Sending Messages + +After `invite` / `accept` completes, each registered channel gets a callable helper on `_`. + +Example: + +```js +const head = _.petname(type, refs, data) +``` + +Each channel helper is a function: + +```js +_.petname = send +``` + +This call automatically creates: + +- `head` +- `meta.time` +- `meta.stack` + +The helper returns the generated `head`, so callers can keep it for later response matching: + +```js +const head = _.petname('request', {}, data) +``` + +So you should not manually build: + +```js +{ head, refs, type, data } +``` + +in net-based communication. + +## `_.petname(type, refs, data)` + +Signature: + +```js +const head = _.petname(type, refs = {}, data = []) +``` + +Example response to an incoming message: + +```js +function io_petname () { + const on_message = { + response: handle_response + } + return protocol + + function protocol (msg) { + const handler = on_message[msg.type] || fail + handler(msg) + } + + function handle_response (msg) { + const head = _.petname('done', { cause: msg.head }, { ok: true }) + } +} +``` + +Use `refs.cause` when a message is derived from another message: + +```js +_.petname('done', { cause: msg.head }, data) +``` + +Use `{}` for UI/root-originated messages: + +```js +_.up('ui_focus', {}, { type: 'wizard_hat', sid: opts.sid }) +``` + +## Recommended Component Pattern + +```js +module.exports = mycomponent + +async function mycomponent (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { io, _ } = net(id) + + io.on = { + up: io_up(), + petname: io_petname() + } + + _.up('render_form', { cause: msg.head }, data) + + const child = await dependency({ ...subs[0] }, io.invite('petname', { up: id })) + + return el + + function io_up () { + const on_message = { + some_type: handle_some_type + } + _.action_bar(msg.type, { cause: msg.head }, msg.data) + _.up('render_form', { cause: msg.head }, data) + function protocol (msg) { + const handler = on_message[msg.type] || fail + handler(msg) + } + } + + function io_petname () { + const on_message = { + some_type: handle_some_type + } + return protocol + + function protocol (msg) { + const handler = on_message[msg.type] || fail + handler(msg) + } + } +} +``` + +## Routing Notes + +`net_helper` forwards automatically based on the recipient in `head`. + +That means components using `net_helper` should not add extra manual forwarding just to move a message across already-connected net channels. + +Use channel helpers directly: + +```js +_.action_bar(msg.type, { cause: msg.head }, msg.data) +_.up('render_form', { cause: msg.head }, data) +``` + +instead of rebuilding a full message object. \ No newline at end of file diff --git a/src/node_modules/net_helper/net_helper.js b/src/node_modules/net_helper/net_helper.js new file mode 100644 index 0000000..8da3ca6 --- /dev/null +++ b/src/node_modules/net_helper/net_helper.js @@ -0,0 +1,55 @@ +module.exports = net + +function net (id) { + const [label, io, _, sub, hub] = [`[${id}@${__filename}]`, { invite, accept, on: {} }, {}, {}, {}] + return { io, _ } + function forward (to, M) { + if (to.startsWith(id)) { + const ups = [...new Set(Object.keys(hub).map(id => hub[id].tx))] + for (const tx of ups) tx(M) + return + } + for (const id of Object.keys(sub)) if (to.startsWith(id)) return sub[id].tx(M) + throw new Error(`${label} unknown recipient "${to}"`) + } + function invite (name, ids) { + if (!io.on[name]) throw new Error(`${label} no protocol handler for "${name}"`) + return Object.assign(invite, { ids }) + function invite (tx) { + const rx = router(sub) + add(name, tx, tx.id, rx, sub) + return rx + } + } + function accept (invite) { + const rx = router(hub) + const tx = invite(Object.assign(rx, { id })) + for (const [name, to] of Object.entries(invite.ids)) { + if (hub[to]) throw new Error(`${label} already connected to "${to}"`) + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + add(name, tx, to, rx, hub) + } + } + function router ($) { + return function rx (M) { + const { head: [by, to] } = M + console.log(`[M]\n${by} \n to: \n ${to}`, M) + if (to !== id) return forward(to, M) + if (!$[by]) throw new Error(`${label} unknown sender "${by}"`) + const { name } = $[by].state + if (!io.on[name]) throw new Error(`${label} no "${name}" protocol for "${to}"`) + io.on[name](M) + } + } + function add (name, tx, to, rx, $) { + const state = { name, to, mid: 0 } + _[name] = send + $[to] = { rx, tx, state } + function send (type, refs = {}, data = null) { + const head = [id, to, state.mid++] + const meta = { time: Date.now(), stack: (new Error().stack) } + tx({ head, refs, type, data, meta }) + return head + } + } +} diff --git a/src/node_modules/net_helper/package.json b/src/node_modules/net_helper/package.json new file mode 100644 index 0000000..702fcb8 --- /dev/null +++ b/src/node_modules/net_helper/package.json @@ -0,0 +1 @@ +{"main": "net_helper.js"} diff --git a/src/node_modules/program/package.json b/src/node_modules/program/package.json new file mode 100644 index 0000000..f14c75f --- /dev/null +++ b/src/node_modules/program/package.json @@ -0,0 +1,5 @@ +{ + "name": "program", + "main": "program.js" + } + \ No newline at end of file diff --git a/src/node_modules/program/program.js b/src/node_modules/program/program.js new file mode 100644 index 0000000..767f9ad --- /dev/null +++ b/src/node_modules/program/program.js @@ -0,0 +1,116 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const form_input = require('form_input') +const input_test = require('input_test') +const form_tile_split_choice = require('form_tile_split_choice') +const form_click_rate_test = require('form_click_rate_test') + +program.form_input = form_input +program.input_test = input_test +program.form_tile_split_choice = form_tile_split_choice +program.form_click_rate_test = form_click_rate_test + +module.exports = program + +async function program (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + variables: onvariables + } + + const { io } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + await sdb.watch(onbatch) + + const parent_handler = { + display_result, + update_data + } + + return el + + // --- Internal Functions --- + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + + function onvariables (data) { + // Dont get why we have this module. + } + + function io_up () { + return function onmessage ({ type, data }) { + const handler = parent_handler[type] || fail + handler(data, type) + } + } + function display_result (data) { + console.log('Display Result:', data) + alert(`Result of action(${data.selected_action ? data.selected_action : 'unknown'}): ${data.result ? data.result : 'no result'}`) + } + function update_data (data) { drive.put('variables/program.json', data) } +} + +// --- Fallback Module --- +function fallback_module () { + return { + api: fallback_instance, + _: { + form_input: { $: '' }, + input_test: { $: '' }, + form_tile_split_choice: { $: '' }, + form_click_rate_test: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + net_helper: { 0: '' } + }, + drive: { + 'style/': { + 'program.css': { + raw: ` + .main { + display: flex; + flex-direction: column; + align-items: center; + } + ` + } + }, + 'variables/': { + 'program.json': { $ref: 'program.json' } + } + } + } + } +} diff --git a/src/node_modules/program/program.json b/src/node_modules/program/program.json new file mode 100644 index 0000000..292677f --- /dev/null +++ b/src/node_modules/program/program.json @@ -0,0 +1,12 @@ +{"change_path": [ + {"name": "select an element", "type": "mandatory", "is_completed": false, "component": "form_input", "status": "default", "data": ""}, + {"name": "step 2", "type": "mandatory", "is_completed": false, "component": "input_test", "status": "default", "data": ""} +], +"Open File": [ + {"name": "open file", "type": "mandatory", "is_completed": false, "component": "form_input", "status": "default", "data": ""} +], +"Settings": [ + {"name": "open settings", "type": "mandatory", "is_completed": false, "component": "form_input", "status": "default", "data": ""}, + {"name": "step 2", "type": "mandatory", "is_completed": false, "component": "input_test", "status": "default", "data": ""} +] +} diff --git a/src/node_modules/program_container/package.json b/src/node_modules/program_container/package.json new file mode 100644 index 0000000..1f6ecf8 --- /dev/null +++ b/src/node_modules/program_container/package.json @@ -0,0 +1 @@ +{"main": "program_container.js"} diff --git a/src/node_modules/program_container/program_container.js b/src/node_modules/program_container/program_container.js new file mode 100644 index 0000000..7e83e15 --- /dev/null +++ b/src/node_modules/program_container/program_container.js @@ -0,0 +1,505 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +// const docs = DOCS(__filename)() +const net = require('net_helper') + +const console_history = require('console_history') +const actions = require('actions') +const tabbed_editor = require('tabbed_editor') +const graph_viewer = require('graph_viewer') +const docs_window = require('docs_window') + +module.exports = program_container + +async function program_container (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+ + + + + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const program_main = shadow.querySelector('.program-container') + const graph_explorer_placeholder = shadow.querySelector('graph-explorer-placeholder') + const actions_placeholder = shadow.querySelector('actions-placeholder') + const tabbed_editor_placeholder = shadow.querySelector('tabbed-editor-placeholder') + const console_placeholder = shadow.querySelector('console-history-placeholder') + const docs_window_placeholder = shadow.querySelector('docs-window-placeholder') + + let console_history_el = null + let docs_window_el = null + let actions_el = null + let tabbed_editor_el = null + let graph_explorer_el = null + + const subs = await sdb.watch(onbatch) + + io.on = { + up: io_up(), + console_history: io_console_history(), + actions: io_actions(), + tabbed_editor: io_tabbed_editor(), + graph_explorer: io_graph_explorer(), + docs_window: io_docs_window() + } + if (invite) io.accept(invite) + + actions_el = await actions({ ...subs[1] }, io.invite('actions', { up: id })) + actions_el.classList.add('actions') + actions_placeholder.replaceWith(actions_el) + + tabbed_editor_el = await tabbed_editor({ ...subs[2] }, io.invite('tabbed_editor', { up: id })) + tabbed_editor_el.classList.add('tabbed-editor') + tabbed_editor_placeholder.replaceWith(tabbed_editor_el) + + docs_window_el = await docs_window({ ...subs[4] }, io.invite('docs_window', { up: id })) + docs_window_el.classList.add('docs-window') + docs_window_el.classList.add('hide') + docs_window_placeholder.replaceWith(docs_window_el) + + graph_explorer_el = await graph_viewer({ ...subs[3] }, io.invite('graph_explorer', { up: id })) + graph_explorer_el.classList.add('graph-explorer') + graph_explorer_placeholder.replaceWith(graph_explorer_el) + + console_history_el = await console_history({ ...subs[0] }, io.invite('console_history', { up: id })) + console_history_el.classList.add('console-history') + console_placeholder.replaceWith(console_history_el) + let console_view = false + let actions_view = false + let graph_explorer_view = false + + if (invite) { + console_history_el.classList.add('hide') + actions_el.classList.add('hide') + tabbed_editor_el.classList.add('show') + graph_explorer_el.classList.add('hide') + + // Send message to root to set doc display handler + _.up('set_doc_display_handler', {}, { callback: on_doc_display }) + } + + if (!invite) { + actions_view = !actions_el.classList.contains('hide') + console_view = !console_history_el.classList.contains('hide') + graph_explorer_view = !graph_explorer_el.classList.contains('hide') + } + update_program_layout() + + return el + + function console_history_toggle_view () { + const next_view = !console_view + set_panel_visibility(console_history_el, next_view) + console_view = next_view + update_program_layout() + } + + function actions_toggle_view (display_data) { + const next_view = resolve_display_state(display_data, actions_view) + set_panel_visibility(actions_el, next_view) + actions_view = next_view + update_program_layout() + } + + function graph_explorer_toggle_view () { + const next_view = !graph_explorer_view + set_panel_visibility(graph_explorer_el, next_view) + graph_explorer_view = next_view + update_program_layout() + } + + function resolve_display_state (display_data, current_view) { + if (typeof display_data === 'boolean') return display_data + if (typeof display_data === 'string') return display_data !== 'none' + if (typeof display_data === 'object' && display_data.display !== undefined) return display_data.display !== 'none' + return !current_view + } + + function set_panel_visibility (panel_el, visible) { + if (visible) { + panel_el.classList.remove('hide') + panel_el.classList.add('show') + } else { + panel_el.classList.remove('show') + panel_el.classList.add('hide') + } + } + + function tabbed_editor_toggle_view (show = true) { + if (show) { + set_panel_visibility(tabbed_editor_el, true) + set_panel_visibility(actions_el, false) + set_panel_visibility(console_history_el, false) + set_panel_visibility(graph_explorer_el, false) + actions_view = false + console_view = false + graph_explorer_view = false + } else { + set_panel_visibility(tabbed_editor_el, false) + } + update_program_layout() + } + + function update_program_layout () { + const tabbed_visible = !tabbed_editor_el.classList.contains('hide') + const graph_visible = !graph_explorer_el.classList.contains('hide') + const actions_visible = !actions_el.classList.contains('hide') + const console_visible = !console_history_el.classList.contains('hide') + const has_primary = tabbed_visible || graph_visible + + let tabbed_row = '0px' + let graph_row = '0px' + let actions_row = '0px' + let console_row = '0px' + + if (tabbed_visible) { + tabbed_row = graph_visible ? 'minmax(120px, 1fr)' : 'minmax(80px, 1fr)' + } + if (graph_visible) { + graph_row = tabbed_visible ? 'minmax(150px, 1fr)' : 'minmax(200px, 1fr)' + } + if (actions_visible) { + if (!has_primary && !console_visible) actions_row = 'minmax(80px, 1fr)' + else actions_row = 'fit-content(260px)' + } + if (console_visible) { + if (!has_primary && !actions_visible) console_row = 'minmax(80px, 1fr)' + else console_row = 'fit-content(260px)' + } + if (!tabbed_visible && !graph_visible && !actions_visible && !console_visible) { + tabbed_row = '1fr' + } + + program_main.style.gridTemplateRows = `${tabbed_row} ${graph_row} ${actions_row} ${console_row}` + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + function inject ({ data }) { sheet.replaceSync(data[0]) } + + function on_doc_display (display_data) { + const { content, sid } = display_data + docs_window_el.classList.remove('hide') + _.docs_window('display_doc', {}, { content, sid }) + } + + // --------- + // PROTOCOLS + // --------- + + function io_console_history () { + return function console_history_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_actions () { + return function actions_protocol (msg) { + const action_handlers = { + selected_action: actions_selected_action, + ui_focus_docs: actions_ui_focus_docs, + ui_focus: actions_forward_up + } + + const handler = action_handlers[msg.type] || actions_forward_up + handler(msg) + + function actions_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function actions_selected_action (msg) { + const { data } = msg + _.up('update_quick_actions_input', msg.head ? { cause: msg.head } : {}, data) + } + + function actions_ui_focus_docs (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function io_tabbed_editor () { + return function tabbed_editor_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_graph_explorer () { + return function graph_explorer_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + function io_docs_window () { + return function docs_window_protocol (msg) { + const action_handlers = { + close_docs: docs_window_close_docs + } + const handler = action_handlers[msg.type] || docs_window_noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function docs_window_close_docs () { docs_window_el.classList.add('hide') } + function docs_window_noop () {} + } + } + + function io_up () { + return function onmessage (msg) { + const action_handlers = { + console_history_toggle: onmessage_console_history_toggle, + graph_explorer_toggle: onmessage_graph_explorer_toggle, + display_actions: onmessage_display_actions, + filter_actions: onmessage_filter_actions, + tab_name_clicked: onmessage_tab_name_clicked, + tab_close_clicked: onmessage_tab_close_clicked, + switch_tab: onmessage_switch_tab, + entry_toggled: onmessage_entry_toggled, + execute_step: onmessage_execute_step, + display_doc: onmessage_display_doc, + load_actions: onmessage_send_actions, + update_actions_for_app: onmessage_send_actions + } + const handler = action_handlers[msg.type] || fail + handler(msg) + + function onmessage_console_history_toggle () { console_history_toggle_view() } + function onmessage_graph_explorer_toggle () { graph_explorer_toggle_view() } + function onmessage_display_actions (msg) { actions_toggle_view(msg.data) } + function onmessage_filter_actions (msg) { _.actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_tab_close_clicked (msg) { + _.tabbed_editor('close_tab', msg.head ? { cause: msg.head } : {}, msg.data) + _.console_history('record_closed_tab', msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_entry_toggled (msg) { _.graph_explorer(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_execute_step (msg) { + if (!msg.data || !Array.isArray(msg.data.commands) || msg.data.commands.length === 0) return + set_panel_visibility(graph_explorer_el, true) + graph_explorer_view = true + update_program_layout() + _.graph_explorer(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_send_actions (msg) { _.actions(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_tab_name_clicked (msg) { + tabbed_editor_toggle_view(true) + _.tabbed_editor('toggle_tab', msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_switch_tab (msg) { + tabbed_editor_toggle_view(true) + _.tabbed_editor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + function onmessage_display_doc (msg) { + docs_window_el.classList.remove('hide') + _.docs_window(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + console_history: { + $: '' + }, + actions: { + $: '' + }, + tabbed_editor: { + $: '' + }, + graph_viewer: { + $: '' + }, + docs_window: { + $: '' + }, + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + + function fallback_instance () { + return { + _: { + console_history: { + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + docs: 'docs', + actions: 'actions' + } + }, + actions: { + 0: '', + mapping: { + style: 'style', + actions: 'actions', + icons: 'icons', + hardcons: 'hardcons', + docs: 'docs' + } + }, + tabbed_editor: { + 0: '', + mapping: { + style: 'style', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + docs: 'docs' + } + }, + graph_viewer: { + 0: '', + mapping: { + theme: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + }, + docs_window: { + 0: '', + mapping: { + style: 'docs_style' + } + }, + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .program-container { + display: grid; + grid-template-columns: minmax(0, 1fr); + min-height: 200px; + height: 100%; + background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); + position: relative; + gap: 0; + padding: 0; + overflow: hidden; + container-type: size; + } + .docs-window { + position: absolute; + inset: 12px; + z-index: 20; + } + .tabbed-editor { + grid-row: 1; + grid-column: 1; + min-height: 0; + min-width: 0; + width: 100%; + height: 100%; + } + .graph-explorer { + grid-row: 2; + grid-column: 1; + min-height: 0; + min-width: 0; + width: 100%; + height: 100%; + } + .console-history { + grid-row: 4; + grid-column: 1; + display: flex; + flex-direction: column; + position: relative; + width: 100%; + height: 100%; + max-height: min(400px, 100%); + min-height: 0; + min-width: 0; + background-color: #161b22; + border: 1px solid #21262d; + border-radius: 6px; + box-sizing: border-box; + overflow: hidden; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + } + .actions { + grid-row: 3; + grid-column: 1; + position: relative; + background-color: #161b22; + border: 1px solid #21262d; + border-radius: 6px; + overflow: hidden; + } + .tabbed-editor { + position: relative; + width: 100%; + min-width: 0; + background-color: #0d1117; + border: 1px solid #21262d; + border-radius: 6px; + overflow: hidden; + } + .show { + display: block; + } + .hide { + display: none; + } + ` + } + }, + 'entries/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'commands/': {}, + 'icons/': {}, + 'scroll/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'active_tab/': {}, + 'runtime/': {}, + 'mode/': {}, + 'undo/': {}, + 'docs_style/': {} + } + } + } +} diff --git a/src/node_modules/quick_actions/README.md b/src/node_modules/quick_actions/README.md new file mode 100644 index 0000000..5f2a2e4 --- /dev/null +++ b/src/node_modules/quick_actions/README.md @@ -0,0 +1 @@ +A command palette component with an input field for searching/filtering actions, step display, and quick action buttons for common operations. diff --git a/src/node_modules/quick_actions/action1.svg b/src/node_modules/quick_actions/action1.svg new file mode 100644 index 0000000..7e08174 --- /dev/null +++ b/src/node_modules/quick_actions/action1.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/quick_actions/action2.svg b/src/node_modules/quick_actions/action2.svg new file mode 100644 index 0000000..0768b44 --- /dev/null +++ b/src/node_modules/quick_actions/action2.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/quick_actions/check.svg b/src/node_modules/quick_actions/check.svg new file mode 100644 index 0000000..125b3ce --- /dev/null +++ b/src/node_modules/quick_actions/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/quick_actions/cross.svg b/src/node_modules/quick_actions/cross.svg new file mode 100644 index 0000000..d836234 --- /dev/null +++ b/src/node_modules/quick_actions/cross.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/node_modules/quick_actions/package.json b/src/node_modules/quick_actions/package.json new file mode 100644 index 0000000..eaf5b95 --- /dev/null +++ b/src/node_modules/quick_actions/package.json @@ -0,0 +1,3 @@ +{ + "main": "quick_actions.js" +} \ No newline at end of file diff --git a/src/node_modules/quick_actions/quick_actions.js b/src/node_modules/quick_actions/quick_actions.js new file mode 100644 index 0000000..a0e729f --- /dev/null +++ b/src/node_modules/quick_actions/quick_actions.js @@ -0,0 +1,695 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = quick_actions + +async function quick_actions (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: iconject, + hardcons: onhardcons, + actions: onactions, + prefs: onprefs + } + + const el = document.createElement('div') + el.style.display = 'flex' + el.style.flex = 'auto' + + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+ +
+
` + const container = shadow.querySelector('.quick-actions-container') + const default_actions = shadow.querySelector('.default-actions') + const text_bar = shadow.querySelector('.text-bar') + const input_wrapper = shadow.querySelector('.input-wrapper') + const slash_prefix = shadow.querySelector('.slash-prefix') + const command_text = shadow.querySelector('.command-text') + const input_field = shadow.querySelector('.input-field') + const confirm_btn = shadow.querySelector('.confirm-btn') + const submit_btn = shadow.querySelector('.submit-btn') + const close_btn = shadow.querySelector('.close-btn') + const step_display = shadow.querySelector('.step-display') + const current_step = shadow.querySelector('.current-step') + const total_steps = shadow.querySelector('.total-step') + const tooltip = shadow.querySelector('.tooltip') + const input_tooltip = shadow.querySelector('.input-tooltip') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + let init = false + let enable_quick_action_tooltips = false + let enable_input_field_tooltips = false + let icons = {} + let hardcons = {} + let defaults = [] + let stored_selected_action = '' + let action_selected = false + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + const ui_actions = [ + create_action('Open Quick Actions', 'Open the quick action input.', activate_input_field), + create_action('Close Quick Actions', 'Close the quick action input.', deactivate_input_field), + create_action('Confirm Quick Action', 'Continue with the selected action.', confirm_action), + create_action('Submit Quick Action', 'Submit the selected action.', submit_action) + ] + register_actions() + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + on_open.info = ui_actions[0].info + on_close.info = ui_actions[1].info + on_confirm.info = ui_actions[2].info + on_submit.info = ui_actions[3].info + text_bar.onclick = docs.wrap_isolated(on_open) + close_btn.onclick = docs.wrap_isolated(on_close) + confirm_btn.onclick = docs.wrap_isolated(on_confirm) + submit_btn.onclick = docs.wrap_isolated(on_submit) + input_field.oninput = oninput + + await sdb.watch(onbatch) + + return el + + function oninput (e) { + const value = e.target.value + if (enable_input_field_tooltips) update_input_tooltip(value) + _.up('filter_actions', {}, value) + } + + function update_input_display (selected_action = null) { + if (selected_action) { + action_selected = true + slash_prefix.style.display = 'inline' + command_text.style.display = 'inline' + command_text.textContent = `#${selected_action.name}` + current_step.textContent = selected_action.current_step ? selected_action.current_step : 1 + total_steps.textContent = selected_action.total_steps ? selected_action.total_steps : 1 + step_display.style.display = 'inline-flex' + + input_field.style.display = 'none' + confirm_btn.style.display = 'flex' + hide_input_tooltip() + } else { + slash_prefix.style.display = 'none' + command_text.style.display = 'none' + input_field.style.display = 'block' + confirm_btn.style.display = 'none' + submit_btn.style.display = 'none' + step_display.style.display = 'none' + input_field.placeholder = 'Type to search actions...' + hide_input_tooltip() + action_selected = false + } + } + + function activate_input_field () { + if (action_selected) return + default_actions.style.display = 'none' + text_bar.style.display = 'none' + + input_wrapper.style.display = 'flex' + input_field.focus() + + if (enable_input_field_tooltips) update_input_tooltip('') + + _.up('display_actions', {}, { display: 'block', reason: 'browse' }) + } + + function io_up () { + return function onmessage (msg) { + const { type, data } = msg + // No need to handle docs_toggle - DOCS module handles it globally + const message_map = { + deactivate_input_field, + show_submit_btn, + update_current_step, + hide_submit_btn, + update_quick_actions_for_app, + update_input_command + } + const handler = message_map[type] || fail + handler(data) + } + } + + function deactivate_input_field (data = {}) { + const reason = data.reason ? data.reason : 'cancel' + + default_actions.style.display = 'flex' + text_bar.style.display = 'flex' + + input_wrapper.style.display = 'none' + + input_field.value = '' + update_input_display() + hide_input_tooltip() + + _.up('display_actions', {}, { display: 'none', reason }) + } + + function show_submit_btn () { + submit_btn.style.display = 'flex' + confirm_btn.style.display = 'none' + } + function hide_submit_btn () { submit_btn.style.display = 'none' } + + function update_current_step (data) { + const current_step_value = data.index !== undefined ? data.index + 1 : 1 + current_step.textContent = current_step_value + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + create_default_actions(defaults) + init = true + } else { + // TODO: update actions + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn(`Invalid message type: ${type}`, { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } + function onhardcons (data) { + hardcons = { + submit: data[0], + cross: data[1], + confirm: data[2] + } + submit_btn.innerHTML = hardcons.submit + close_btn.innerHTML = hardcons.cross + confirm_btn.innerHTML = hardcons.confirm + } + function iconject (data) { icons = data } + + function onactions (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + defaults = vars + register_actions() + create_default_actions(defaults) + } + + function onprefs (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + enable_input_field_tooltips = vars.input_field + enable_quick_action_tooltips = vars.quick_actions + } + + function create_default_actions (actions) { + default_actions.replaceChildren() + actions.forEach(create_action_button) + } + + function create_action_button (action) { + const btn = document.createElement('div') + btn.classList.add('action-btn') + if (icons[action.icon] === undefined) { + const texon = action.name.substring(0, 2) + btn.innerHTML = texon + } else { + btn.innerHTML = icons[action.icon] + } + if (enable_quick_action_tooltips) { + btn.onmouseenter = on_action_btn_mouseenter + btn.onmouseleave = hide_tooltip + } + on_action_click.info = action.info + on_action_click.opts = { state: { name: action.name } } + btn.onclick = docs.wrap_isolated(on_action_click) + default_actions.appendChild(btn) + + function on_action_click (event, $) { $($.state.name) } + function on_action_btn_mouseenter () { show_tooltip(btn, action.name) } + } + + function update_input_tooltip (value) { + if (!value || value.trim() === '') { + hide_input_tooltip() + return + } + const tooltip_text = get_tooltip_text(value) + if (tooltip_text) { + show_input_tooltip(tooltip_text) + } else { + hide_input_tooltip() + } + } + + function get_tooltip_text (value) { + const lower_value = value.toLowerCase().trim() + if (lower_value.length === 0) return null + if (defaults.length > 0) { + const matching = defaults.filter(matches_action_for_tooltip) + if (matching.length > 0) { + const names = matching.map(get_action_name) + return `Found ${matching.length} action${matching.length > 1 ? 's' : ''}: ${names.join(', ')}` + } + } + return 'No actions found. Try a different search term.' + + function matches_action_for_tooltip (action) { return matches_action(action, lower_value) } + function get_action_name (action) { return action.name } + } + + function matches_action (action, search_term) { return action.name.toLowerCase().includes(search_term) } + + function show_input_tooltip (text) { + input_tooltip.textContent = text + input_tooltip.style.display = 'block' + position_input_tooltip() + } + + function hide_input_tooltip () { input_tooltip.style.display = 'none' } + + function position_input_tooltip () { + const input_rect = input_field.getBoundingClientRect() + const wrapper_rect = input_wrapper.getBoundingClientRect() + const tooltip_rect = input_tooltip.getBoundingClientRect() + const left = input_rect.left - wrapper_rect.left + (input_rect.width / 2) - (tooltip_rect.width / 2) + const top = input_rect.top - wrapper_rect.top - tooltip_rect.height - 8 + input_tooltip.style.left = `${left}px` + input_tooltip.style.top = `${top}px` + } + + function update_quick_actions_for_app (data) { + if (data) { + drive.put('actions/default.json', data) + } + } + + function update_input_command (command) { + if (action_selected) return + stored_selected_action = command + if (input_wrapper.style.display === 'none') { + default_actions.style.display = 'none' + text_bar.style.display = 'none' + input_wrapper.style.display = 'flex' + input_field.focus() + if (enable_input_field_tooltips) update_input_tooltip('') + } + + // Find the action that matches the command + const matching_action = defaults.find(matches_selected_command) + const selected = matching_action || command + + if (matching_action) { + const pass_data = { + name: matching_action.name, + current_step: 1, + total_steps: matching_action.steps ? matching_action.steps.length : 1 + } + update_input_display(pass_data) + } else { + const pass_data = { + name: typeof command === 'string' ? command : command.name, + current_step: 1, + total_steps: 3 + } + update_input_display(pass_data) + } + + _.up('display_actions', {}, { display: 'none', reason: 'selected' }) + _.up('activate_steps_wizard', {}, stored_selected_action) + + function matches_selected_command (action) { + const target = typeof command === 'string' ? command : command?.name + return action.name === target + } + } + + function create_action (name, info, run) { + return { name, info, icon: 'action', status: { hidden: true }, steps: [], run } + } + + function register_actions () { + docs.register_actions(ui_actions.concat(defaults.map(bind_action))) + + function bind_action (action) { + return { ...action, run: select_action } + + function select_action () { _.up('update_quick_actions_input', {}, action) } + } + } + + function on_open (event, $) { $('Open Quick Actions') } + function on_close (event, $) { $('Close Quick Actions') } + function on_confirm (event, $) { $('Confirm Quick Action') } + function on_submit (event, $) { $('Submit Quick Action') } + function confirm_action () { _.up('activate_steps_wizard', {}, stored_selected_action) } + function submit_action () { _.up('action_submitted', {}, null) } + + function show_tooltip (btn, name) { + tooltip.textContent = name + tooltip.style.display = 'block' + const btn_rect = btn.getBoundingClientRect() + const container_rect = container.getBoundingClientRect() + const tooltip_rect = tooltip.getBoundingClientRect() + const left = btn_rect.left - container_rect.left + (btn_rect.width / 2) - (tooltip_rect.width / 2) + const top = btn_rect.top - container_rect.top - tooltip_rect.height - 8 + tooltip.style.left = `${left}px` + tooltip.style.top = `${top}px` + } + + function hide_tooltip () { tooltip.style.display = 'none' } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + '0.svg': { + $ref: 'action1.svg' + }, + '1.svg': { + $ref: 'action2.svg' + }, + '2.svg': { + $ref: 'action1.svg' + }, + '3.svg': { + $ref: 'action2.svg' + }, + '4.svg': { + $ref: 'action1.svg' + } + }, + 'hardcons/': { + 'submit.svg': { + $ref: 'submit.svg' + }, + 'close.svg': { + $ref: 'cross.svg' + }, + 'confirm.svg': { + $ref: 'check.svg' + } + }, + 'actions/': { + 'default.json': { + raw: JSON.stringify([]) + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .quick-actions-container { + display: flex; + flex: auto; + flex-direction: row; + align-items: center; + background: #191919; + border-radius: 20px; + gap: 8px; + min-width: 200px; + position: relative; + } + .default-actions { + display: flex; + flex-direction: row; + align-items: center; + gap: 4px; + padding: 0 4px; + } + .action-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + } + .action-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + .text-bar { + flex: 1; + height: 24px; + margin: 4px; + border-radius: 16px; + background: #131315; + cursor: pointer; + user-select: none; + } + .text-bar:hover { + background: #1a1a1c; + } + .input-wrapper { + display: flex; + flex: 1; + align-items: center; + background: #131315; + border-radius: 16px; + width: auto; + height: 30px; + border: 1px solid #3c3c3c; + } + .input-wrapper:focus-within { + border-color: #4285f4; + background: #1a1a1c; + } + .input-display { + display: flex; + flex: 1; + align-items: center; + padding: 0 12px; + min-height: 32px; + position: relative; + } + .slash-prefix { + color: #a6a6a6; + font-size: 14px; + margin-right: 4px; + display: none; + } + .command-text { + color: #e8eaed; + font-size: 14px; + background: #2d2d2d; + border: 1px solid #4285f4; + border-radius: 4px; + padding: 2px 6px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + display: none; + } + .input-field { + flex: 1; + min-height: 32px; + background: transparent; + border: none; + color: #e8eaed; + padding: 0 12px; + font-size: 14px; + outline: none; + } + .input-field::placeholder { + color: #a6a6a6; + } + .submit-btn { + display: none; + align-items: center; + justify-content: center; + background: #ffffff00; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: white; + min-width: 32px; + height: 32px; + margin-right: 4px; + font-size: 12px; + } + .submit-btn:hover { + background: #ffffff00; + } + .confirm-btn { + display: none; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + min-width: 32px; + height: 32px; + margin-right: 4px; + font-size: 12px; + } + .confirm-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + .close-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + padding: 6px; + border-radius: 50%; + cursor: pointer; + color: #a6a6a6; + min-width: 32px; + height: 32px; + } + .close-btn:hover { + background: rgba(255, 255, 255, 0.1); + } + svg { + width: 16px; + height: 16px; + } + .step-display { + display: inline-flex; + align-items: center; + gap: 2px; + margin-left: 8px; + background: #2d2d2d; + border: 1px solid #666; + border-radius: 4px; + padding: 1px 6px; + font-size: 12px; + color: #fff; + font-family: monospace; + } + .current-step { + color:#f0f0f0; + } + .step-separator { + color: #888; + } + .total-step { + color: #f0f0f0; + } + .hide { + display: none; + } + .tooltip { + position: absolute; + background: #2d2d2d; + color: #e8eaed; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + pointer-events: none; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid #3c3c3c; + } + .tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #2d2d2d; + } + .input-tooltip { + position: absolute; + background: #2d2d2d; + color: #e8eaed; + padding: 6px 12px; + border-radius: 4px; + font-size: 12px; + white-space: normal; + pointer-events: none; + z-index: 1001; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + border: 1px solid #4285f4; + max-width: 300px; + word-wrap: break-word; + } + .input-tooltip::after { + content: ''; + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + border: 4px solid transparent; + border-top-color: #4285f4; + } + ` + } + }, + 'prefs/': { + 'tooltips.json': { + raw: JSON.stringify({ + quick_actions: true, + input_field: false + }) + } + } + } + } + } +} diff --git a/src/node_modules/quick_actions/submit.svg b/src/node_modules/quick_actions/submit.svg new file mode 100644 index 0000000..5abe6cb --- /dev/null +++ b/src/node_modules/quick_actions/submit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/node_modules/quick_editor/package.json b/src/node_modules/quick_editor/package.json new file mode 100644 index 0000000..0268ca1 --- /dev/null +++ b/src/node_modules/quick_editor/package.json @@ -0,0 +1 @@ +{"main": "quick_editor.js"} diff --git a/src/node_modules/quick_editor/quick_editor.js b/src/node_modules/quick_editor/quick_editor.js new file mode 100644 index 0000000..219acca --- /dev/null +++ b/src/node_modules/quick_editor/quick_editor.js @@ -0,0 +1,440 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const { resource } = require('helpers') + +module.exports = quick_editor +let is_called +const nesting = 0 + +async function quick_editor (opts) { + // ---------------------------------------- + let init; let data; let port; let labels; let nesting_limit; let top_first; let select = [] + const current_data = {} + + const { sdb, io, net } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + // ---------------------------------------- + const el = document.createElement('div') + el.classList.add('quick-editor') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` + +
+ +
` + + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const menu_btn = shadow.querySelector('.dots-button') + const menu = shadow.querySelector('.quick-menu') + const import_btn = shadow.querySelector('.button.import') + const export_btn = shadow.querySelector('.button.export') + const input = shadow.querySelector('input') + const apply_btn = shadow.querySelector('.button') + // ---------------------------------------- + // EVENTS + // ---------------------------------------- + await sdb.watch(onbatch) + menu_btn.onclick = on_menu_btn_click + + function on_menu_btn_click () { menu_click(false) } + + if (is_called) { + apply_btn.onclick = apply + menu_btn.onclick = on_called_menu_btn_click + + function on_called_menu_btn_click () { menu_click(true) } + + labels = ['Nodes', 'Types', 'Files'] + nesting_limit = nesting + 3 + top_first = 0 + } else { + apply_btn.onclick = on_apply_switch_click + input.onchange = upload + import_btn.onclick = on_import_btn_click + export_btn.onclick = on_export_btn_click + + function on_apply_switch_click () { port.postMessage({ type: 'swtch', data: [{ name: current_data.Types.trim(), type: current_data.Names.trim() }] }) } + function on_import_btn_click () { input.click() } + function on_export_btn_click () { + if (current_data.radio.name === 'Names') { + port.postMessage({ type: 'export_db', data: [{ name: current_data.Names.trim(), type: current_data.Types.trim() }] }) + } else { + port.postMessage({ type: 'export_root', data: [{ name: current_data.Root.trim(), type: current_data.Nodes.trim() }] }) + } + } + + menu.classList.add('admin') + labels = ['Root', 'Types', 'Names', 'Nodes', 'Files', 'Entries'] + nesting_limit = nesting + 6 + top_first = 1 + select = [1, 0, 1, 0, 0, 0] + } + + // ---------------------------------------- + // IO + // ---------------------------------------- + const item = resource() + io.on(register_port_channel) + + function register_port_channel (port) { + const { by, to } = port + item.set(port.to, port) + + port.onmessage = on_port_message + + function on_port_message (event) { + const txt = event.data + const key = `[${by} -> ${to}]` + console.log(key) + data = txt + if (init) { + menu_click(false) + init = false + menu_click(false) + } + } + } + + await io.at(net.page.id) + is_called = true + return el + + // ---------------------------------------- + // FUNCTIONS + // ---------------------------------------- + function upload (e) { + const file = e.target.files[0] + const reader = new FileReader() + reader.onload = on_reader_load + + function on_reader_load (event) { + const content = event.target.result + try { + data = JSON.parse(content) + console.log(file) + if (current_data.radio.name === 'Names') { port.postMessage({ type: 'import_db', data: [data] }) } else { port.postMessage({ type: 'import_root', data: [data, file.name.split('.')[0]] }) } + } catch (err) { + console.error('Invalid JSON file', err) + } + } + + reader.readAsText(file) + } + function make_btn (name, classes, key, nesting) { + const btn = document.createElement('button') + if (select[nesting]) { + btn.innerHTML = ` + ${name} + ` + const input = btn.querySelector('input') + input.onchange = on_radio_input_change + + function on_radio_input_change () { radio_change(input) } + } else { btn.textContent = name } + btn.classList.add(...classes.split(' ')) + btn.setAttribute('tab', name.replaceAll(/[^A-Za-z0-9]/g, '')) + btn.setAttribute('key', key) + btn.setAttribute('title', name) + return btn + } + function make_tab (id, classes, sub_classes, nesting = 0) { + const tab = document.createElement('div') + tab.classList.add(...classes.split(' '), id.replaceAll(/[^A-Za-z0-9]/g, '')) + + let height + if (nesting % 2 === top_first) height = 565 - ((nesting + 1) * 30) + 'px' + else tab.style.maxWidth = 700 - ((nesting + 1) * 47) + 'px' + + tab.innerHTML = ` +
+
+
+
+ ` + + return tab + } + function make_textarea (id, classes, value, nesting) { + const textarea = document.createElement('textarea') + textarea.id = id.replaceAll(/[^A-Za-z0-9]/g, '') + textarea.classList.add(...classes.split(' ')) + textarea.value = typeof (value) === 'object' ? JSON.stringify(value, null, 2) : value + textarea.placeholder = 'Type here...' + textarea.style.width = 700 - ((nesting + 2) * 47) + 'px' + return textarea + } + function radio_change (radio) { + current_data.radio && (current_data.radio.checked = false) + current_data.radio = radio + } + async function menu_click (call) { + port = await item.get(net.page.id) + menu.classList.toggle('hidden') + if (init) { return } + init = true + + const old_box = menu.querySelector('.tab-content') + old_box && old_box.remove() + + const box = make_tab('any', 'tab-content active' + (top_first ? '' : ' sub'), ['btns', 'tabs']) + menu.append(box) + make_tabs(box, data, nesting) + } + function make_tabs (box, data, nesting) { + const local_nesting = nesting + 1 + const not_last_nest = local_nesting !== nesting_limit + let sub = '' + if (local_nesting % 2 === top_first) { sub = ' sub' } + const btns = box.querySelector('.btns') + const tabs = box.querySelector('.tabs') + Object.entries(data).forEach(create_tab_entry) + + function create_tab_entry (entry, i) { + const [key, value] = entry + let first = '' + if (!i) { + first = ' active' + current_data[labels[nesting]] = key + } + + const btn = make_btn(key, `tab-button${first}`, labels[nesting], nesting) + const tab = make_tab(key, `tab-content${sub + first}`, ['btns', 'tabs'], local_nesting) + btn.onclick = on_tab_button_click + + function on_tab_button_click () { tab_btn_click(btn, btns, tabs, '.root-tabs > .tab-content', 'node', key) } + + btns.append(btn) + tabs.append(tab) + if (typeof (value) === 'object' && value !== null && not_last_nest && Object.keys(value).length) { make_tabs(tab, value, local_nesting) } else { + const textarea = make_textarea(key, `subtab-textarea${first}`, value, local_nesting) + tab.append(textarea) + } + } + } + function tab_btn_click (btn, btns, tabs) { + btns.querySelector('.active').classList.remove('active') + tabs.querySelector(':scope > .active').classList.remove('active') + + btn.classList.add('active') + const tab = tabs.querySelector('.' + btn.getAttribute('tab')) + tab.classList.add('active') + current_data[btn.getAttribute('key')] = btn.textContent + + recurse(tab) + function recurse (tab) { + const btn = tab.querySelector('.btns > .active') + if (!btn) { return } + current_data[btn.getAttribute('key')] = btn.textContent + const sub_tab = tab.querySelector('.tabs > .active') + recurse(sub_tab) + } + } + + function apply () { + let raw = shadow.querySelector('.tab-content.active .tab-content.active textarea.active').value + if (current_data.Files.split('.')[1] === 'json') { raw = JSON.parse(raw) } + port.postMessage({ + type: 'put', + data: [ + current_data.dataset + current_data.file, + raw, + current_data.node + ] + }) + } + + function inject (data) { sheet.replaceSync(data[0]) } + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } +} + +function fallback_module () { + return { + api: fallback_instance + } + function fallback_instance () { + return { + drive: { + 'style/': { + 'quick_editor.css': { + raw: ` + .dots-button { + border: none; + font-size: 24px; + cursor: pointer; + line-height: 1; + background-color: white; + letter-spacing: 1px; + padding: 3px 5px; + border-radius: 20%; + box-shadow: 0 2px 4px rgba(0,0,0,0.3); + } + + .quick-menu { + display: flex; + position: absolute; + top: 100%; + right: 0; + background: white; + padding: 8px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + white-space: nowrap; + z-index: 10; + width: fit-content; + } + *{ + box-sizing: border-box; + } + + .hidden { + display: none; + } + + .btns::before { + display: none; + content: var(--before-content); + font-weight: bold; + color: white; + background: #4CAF50; + padding: 2px 6px; + border-radius: 4px; + position: absolute; + margin-left: -10px; + margin-top: -20px; + } + .btns:hover { + border: 2px solid #4CAF50; + } + .btns:hover::before { + display: block; + } + .btns{ + display: flex; + margin-bottom: 8px; + overflow-x: auto; + background: #d0f0d0; + } + .sub > .btns { + display: flex; + flex-direction: column; + gap: 4px; + max-height: 400px; + overflow-y: auto; + min-width: fit-content; + margin-right: 8px; + background: #d0d2f0ff; + } + + .tab-button { + flex: 1; + padding: 6px; + background: #eee; + border: none; + cursor: pointer; + border-bottom: 2px solid transparent; + max-width: 70px; + width: fit-content; + text-overflow: ellipsis; + overflow: hidden; + min-width: 70px; + min-height: 29px; + position: relative; + text-align: left; + } + .tab-button.active { + background: #fff; + border-bottom: 2px solid #4CAF50; + } + .sub > div > .tab-button.active { + border-bottom: 2px solid #2196F3; + } + .tab-content { + display: none; + max-width: 700px; + background: #d0d2f0ff; + } + .tab-content.active { + display: block; + } + .tab-content.sub.active{ + display: flex; + align-items: flex-start; + } + + textarea { + width: 500px; + max-width: 560px; + height: 400px; + display: block; + resize: vertical; + } + + .button { + display: block; + margin-top: 10px; + padding: 5px 10px; + background-color: #4CAF50; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + height: fit-content; + self-align: end; + width: 100%; + } + .btn-box { + border-right: 1px solid #ccc; + padding-right: 10px; + } + .tabs{ + border-left: 2px solid #ccc; + border-top: 1px solid #ccc; + } + button:has(input[type="radio"]:checked){ + background: #45abffff; + } + button > input[type="radio"]{ + width: 12px; + height: 12px; + border: 2px solid #555; + border-radius: 50%; + display: inline-block; + position: relative; + cursor: pointer; + margin: 0; + } + ` + } + } + } + } + } +} diff --git a/src/node_modules/steps_wizard/README.md b/src/node_modules/steps_wizard/README.md new file mode 100644 index 0000000..7acde73 --- /dev/null +++ b/src/node_modules/steps_wizard/README.md @@ -0,0 +1 @@ +A step indicator component that displays a horizontal list of clickable step buttons with status states (default, completed, disabled, optional, error). diff --git a/src/node_modules/steps_wizard/guide.md b/src/node_modules/steps_wizard/guide.md new file mode 100644 index 0000000..4623889 --- /dev/null +++ b/src/node_modules/steps_wizard/guide.md @@ -0,0 +1,89 @@ +# Steps Wizard Developer Guide + +This guide is intended for developers working with the steps wizard component to understand its structure, usage, and available statuses. + +--- + +## Overview + +The steps wizard is a UI component that shows progress across multiple steps of a process. Each step has a status (e.g., pending, completed, error), and may include additional information such as optional flags or errors. + +--- + +## Status Types + +Each step can have one of the following statuses: + +### 1. `pending(default status)` +- Default status. +- Appears clickable if it’s the first step or the previous one is `completed` or `optional`. +- Greyed out if it's not reachable yet. + +### 2. `optional` +- Styled with **yellow** background and border. +- Allows skipping the step and still moving forward to the next. +- The step number is highlighted in yellow. +- Used when the step is not required to continue the process. + +### 3. `error` +- Styled with **red** background and border. +- Indicates invalid or incomplete user input. +- Clicking it should bring focus to the error area and optionally display a tooltip/message. + +### 4. `completed` +- Styled with **green** background and border. +- The step number is replaced with a ✔️ tick mark. +- User can click to revisit or edit the completed step. + +--- + +## Step Accessibility Rules + +1. **First step** is always active/clickable unless disabled explicitly. +2. A step becomes **clickable** if: + - The previous step is `completed` or `optional`. +3. A step is **disabled** (unclickable) if: + - It is not the current or next possible step. + - It hasn't yet met conditions to be accessed. + - It appears grey with reduced opacity. + +--- + +## Interactivity + +Clicking on a step will: +- Change its status to `completed` (with green tick). +- Move to the next step (if available). +- Allow backward navigation to completed steps. + +--- + +## 🧪 Example Status Flow + +| Step | Status | Notes | +|------|-------------|---------------------------------------------------| +| 1 | `completed` | Shows tick, clickable to revisit | +| 2 | `optional` | Clickable, yellow style, can skip | +| 3 | `pending` | Clickable if step 2 is `completed` or `optional` | +| 4 | `error` | Red border, must fix before proceeding | + +--- + +## Style Notes + +| Status | Color | Icon/Text | +|------------|-----------|-----------------------| +| pending | Grey | Number inside circle | +| optional | Yellow | Number + `*` suffix | +| error | Red | Number + error style | +| completed | Green | ✔️ tick instead of number | + +--- + +## Example Step HTML + +```html + +``` \ No newline at end of file diff --git a/src/node_modules/steps_wizard/package.json b/src/node_modules/steps_wizard/package.json new file mode 100644 index 0000000..48d1c8e --- /dev/null +++ b/src/node_modules/steps_wizard/package.json @@ -0,0 +1,4 @@ +{ + "name": "steps-wizard", + "main": "steps_wizard.js" +} diff --git a/src/node_modules/steps_wizard/steps_wizard.js b/src/node_modules/steps_wizard/steps_wizard.js new file mode 100644 index 0000000..2a4e57b --- /dev/null +++ b/src/node_modules/steps_wizard/steps_wizard.js @@ -0,0 +1,205 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = steps_wizard + +async function steps_wizard (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + let currentActiveStep = 0 + let current_steps = [] + const click_state = { index: 0 } + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const steps_wizard_main = shadow.querySelector('.steps-wizard') + const steps_entries = shadow.querySelector('.steps-slot') + const select_step_action = { + name: 'Select Step', + info: 'Open the selected action step.', + icon: 'step', + status: { hidden: true }, + steps: [], + run: select_step + } + docs.register_actions([select_step_action]) + on_step_click.info = select_step_action.info + on_step_click.opts = { state: click_state } + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage ({ type, data }) { + // docs_toggle handled globally by DOCS module + if (type === 'init_data' && data) { + render_steps(data, true) + } + } + } + + function render_steps (steps, auto_focus_first) { + if (!steps) { return } + current_steps = steps + + const is_single_step = steps.length === 1 + steps_wizard_main.style.display = is_single_step ? 'none' : '' + + steps_entries.innerHTML = '' + currentActiveStep = 0 + + steps.forEach(create_step_button) + + function create_step_button (step, index) { + const btn = document.createElement('button') + btn.className = 'step-button' + btn.textContent = step.name + (step.type === 'optional' ? ' *' : '') + btn.title = btn.textContent + btn.setAttribute('data-step', index + 1) + + const accessible = can_access(index, steps) + + let status = 'default' + if (!accessible) status = 'disabled' + else if (step.is_completed) status = 'completed' + else if (step.status === 'error') status = 'error' + else if (step.type === 'optional') status = 'optional' + + btn.classList.add(`step-${status}`) + + if (index === currentActiveStep - 1 && index > 0) { + btn.classList.add('back') + } + if (index === currentActiveStep + 1 && index < steps.length - 1) { + btn.classList.add('next') + } + if (index === currentActiveStep) { + btn.classList.add('active') + } + + btn.onclick = docs.wrap_isolated(on_step_click) + + steps_entries.appendChild(btn) + + if (auto_focus_first && index === 0) { + btn.classList.add('active') + center_step(btn) + _.up('step_clicked', {}, { ...step, index: 0, total_steps: steps.length, is_accessible: accessible }) + } + } + } + + function on_step_click (event, $) { + $.state.index = Number(event.currentTarget.dataset.step) - 1 + $('Select Step') + } + + function select_step () { + const index = click_state.index + const step = current_steps[index] + const accessible = can_access(index, current_steps) + currentActiveStep = index + center_step(steps_entries.children[index]) + render_steps(current_steps, false) + _.up('step_clicked', {}, { ...step, index, total_steps: current_steps.length, is_accessible: accessible }) + } + + function center_step (step_button) { + const container_width = steps_entries.clientWidth + const step_left = step_button.offsetLeft + const step_width = step_button.offsetWidth + + const center_position = step_left - (container_width / 2) + (step_width / 2) + + steps_entries.scrollTo({ + left: center_position, + behavior: 'smooth' + }) + } + + function can_access (index, steps) { + for (let i = 0; i < index; i++) { + if (!steps[i].is_completed && steps[i].type !== 'optional') { + return false + } + } + + return true + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + + function inject (data) { sheet.replaceSync(data[0]) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'stepswizard.css': { + $ref: 'stepswizard.css' + } + } + } + } + } +} diff --git a/src/node_modules/steps_wizard/stepswizard.css b/src/node_modules/steps_wizard/stepswizard.css new file mode 100644 index 0000000..2e2273f --- /dev/null +++ b/src/node_modules/steps_wizard/stepswizard.css @@ -0,0 +1,240 @@ +/* Slim scrollbar styles for steps-wizard */ +.steps-wizard { +display: flex; + flex-direction: column; + width: 100%; + height: 72px; + background: #131315; + position: relative; + scrollbar-width: thin; + scrollbar-color: transparent transparent; +} + +/* Webkit scrollbar styling (Chrome, Safari, Edge) */ +.steps-wizard::-webkit-scrollbar { + width: 4px; +} + +.steps-wizard::-webkit-scrollbar-track { + background: transparent; +} + +.steps-wizard::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 2px; + transition: background-color 0.3s ease; +} + +/* Show scrollbar on hover */ +.steps-wizard:hover { + scrollbar-color: rgba(255, 255, 255, 0.3) transparent; +} + +.steps-wizard:hover::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); +} + +/* Active scrollbar (when dragging) */ +.steps-wizard::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.5); +} + +.steps-wizard::-webkit-scrollbar-thumb:active { + background: rgba(255, 255, 255, 0.7); +} + +/* Remove scrollbar arrows/buttons */ +.steps-wizard::-webkit-scrollbar-button { + display: none; +} + +/* Firefox scrollbar on hover */ +@supports (scrollbar-width: thin) { + .steps-wizard:hover { + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.3) transparent; + } +} + +.space{ + height: inherit; +} + +.steps-container { + position: relative; + width: 100%; + overflow: hidden; +} + +.steps-slot { + display: flex; + gap: 8px; + padding: 5px; + overflow-x: auto; + scroll-behavior: smooth; +} + +/* Fade edges to indicate more content */ +.steps-container::before, +.steps-container::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + width: 30px; + z-index: 5; + pointer-events: none; + transition: opacity 0.3s ease; + opacity: 0; +} + +.steps-container::before { + left: 0; + background: linear-gradient(to right, #131315, transparent); +} + +.steps-container::after { + right: 0; + background: linear-gradient(to left, #131315, transparent); +} + +.steps-container.has-left-overflow::before, +.steps-container.has-right-overflow::after { + opacity: 1; +} + +.step-button { + position: relative; + display: block; + cursor: pointer; + font-size: 14px; + padding: 10px 8px 10px 44px; + margin: 10px 0; + border-radius: 12px; + font-weight: 500; + transition: background-color 0.3s, transform 0.2s; + border: 2px solid; + width: 120px; + min-width: 120px; + box-sizing: border-box; + flex-shrink: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + line-height: 20px; +} + +.step-button:before { + content: attr(data-step); + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + width: 18px; + height: 18px; + border: 1px solid white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 11px; + flex-shrink: 0; + z-index: 1; +} + +/* Navigation arrows */ +.step-button.back::after { + content: "◄"; + position: absolute; + left: 0px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + font-weight: bold; + color: inherit; + animation: pulse-left 2s infinite; +} + +.step-button.next::after { + content: "►"; + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + font-size: 12px; + font-weight: bold; + color: inherit; + animation: pulse-right 2s infinite; +} + +/* Default (active step) */ +.step-default { + background-color: #224B24; + border-color: #299910; + color: white; +} +.step-default:before { + background-color: #224B24; + color: white; + border: 1px solid white; +} + +/* Optional (yellow) */ +.step-optional { + background-color: #4b3f22; + border-color: #d0a510; + color: #f4c842; +} +.step-optional:before { + background-color: #4b3f22; + border-color: #d0a510; + color: #f4c842; +} + +/* Error (red) */ +.step-error { + background-color: #4b1e1e; + border-color: #e53935; + color:rgb(248, 68, 65); +} +.step-error:before { + background-color: #4b1e1e; + border-color: #e53935; + color: rgb(248, 68, 65); +} + +/* Completed (green with checkmark) */ +.step-completed { + background-color: #224B24; + border-color: #299910; + color: white; +} +.step-completed:before { + content: "✔"; + background-color: #224B24; + border: 1px solid white; + color: white; +} + +/* Disabled (gray) */ +.step-disabled { + background-color: #444; + border-color: #444; + color: #ccc; +} +.step-disabled:before { + background-color: #444; + border-color: #ccc; + color: #ccc; +} + +/* Visibility */ +.hide { + display: none; +} + +.show { + display: block; +} \ No newline at end of file diff --git a/src/node_modules/tab_group/package.json b/src/node_modules/tab_group/package.json new file mode 100644 index 0000000..15d837c --- /dev/null +++ b/src/node_modules/tab_group/package.json @@ -0,0 +1,4 @@ +{ + "name": "tab_group", + "main": "tab_group.js" +} diff --git a/src/node_modules/tab_group/style/tab_group.css b/src/node_modules/tab_group/style/tab_group.css new file mode 100644 index 0000000..3525197 --- /dev/null +++ b/src/node_modules/tab_group/style/tab_group.css @@ -0,0 +1,45 @@ +.tab-group { + --highlight-color: rgba(103, 195, 255, 0.85); + --highlight-color-bg: rgba(103, 195, 255, 0.25); + --highlight-color-border: rgba(103, 195, 255, 0.5); + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: #1a1a1d; + min-height: 0; + border: 1px solid transparent; + transition: border-color 0.15s ease; +} + +/* When this tab group's tile is focused */ +.tab-group.tile-focused { + border-color: var(--highlight-color); +} + +/* Program container at TOP - takes all available space */ +.program-container { + flex: 1 1 auto; + min-height: 0; + height: 100%; + overflow: hidden; +} + +/* Action executor (forms/wizard) - hidden when empty */ +.action-executor { + flex: 0 0 auto; +} + +/* Action bar (quick actions) - above tabs */ +.action-bar { + flex: 0 0 auto; + width: 100%; + border-top: 1px solid rgba(255, 255, 255, 0.1); +} + +/* Tabs - at the bottom */ +.tabs { + flex: 0 0 auto; + width: 100%; + border-top: 1px solid rgba(255, 255, 255, 0.08); +} diff --git a/src/node_modules/tab_group/tab_group.js b/src/node_modules/tab_group/tab_group.js new file mode 100644 index 0000000..b2326fe --- /dev/null +++ b/src/node_modules/tab_group/tab_group.js @@ -0,0 +1,349 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const tabs = require('tabs') +const program_container = require('program_container') +const action_bar = require('action_bar') +const action_executor = require('action_executor') + +module.exports = tab_group + +async function tab_group (opts, invite) { + console.error('tab_group: initializing with opts', opts.sid) + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+
+
+ ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const tab_group_container = shadow.querySelector('.tab-group') + const program_container_slot = shadow.querySelector('.program-container-slot') + const action_executor_slot = shadow.querySelector('.action-executor-slot') + const action_bar_slot = shadow.querySelector('.action-bar-slot') + const tabs_slot = shadow.querySelector('.tabs-slot') + + const subs = await sdb.watch(onbatch) + console.error('tab_group: subs ready', subs) + + let tabs_el = null + let program_container_el = null + let action_bar_el = null + let action_executor_el = null + + io.on = { + up: io_up(), + tabs: io_tabs(), + program_container: io_program_container(), + action_bar: io_action_bar(), + action_executor: io_action_executor() + } + if (invite) { + console.error('tab_group: accepting invite') + io.accept(invite) + } + + console.error('tab_group: creating program_container') + program_container_el = await program_container({ ...subs[0], ids: { up: id } }, io.invite('program_container', { up: id })) + program_container_el.classList.add('program-container') + program_container_slot.replaceWith(program_container_el) + console.error('tab_group: program_container created') + + console.error('tab_group: creating action_executor') + action_executor_el = await action_executor({ ...subs[1], ids: { up: id } }, io.invite('action_executor', { up: id })) + action_executor_el.classList.add('action-executor') + action_executor_slot.replaceWith(action_executor_el) + console.error('tab_group: action_executor created') + + console.error('tab_group: creating tabs') + tabs_el = await tabs({ ...subs[2], ids: { up: id } }, io.invite('tabs', { up: id })) + tabs_el.classList.add('tabs') + tabs_slot.replaceWith(tabs_el) + console.error('tab_group: tabs created') + + console.error('tab_group: creating action_bar') + action_bar_el = await action_bar({ ...subs[3], ids: { up: id } }, io.invite('action_bar', { up: id })) + action_bar_el.classList.add('action-bar') + action_bar_slot.replaceWith(action_bar_el) + console.error('tab_group: action_bar created') + + console.error('tab_group: initialization complete') + return el + + // --------------------------- + // BATCH HANDLER + // --------------------------- + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + function fail ({ data, type }) { console.error('tab_group: unhandled batch message', { type, data }) } + + // --------------------------- + // MESSAGE FROM ROOT (tile_manager) + // --------------------------- + + function io_up () { + const on = { + update_quick_actions_for_app, + update_steps_wizard_for_app, + update_actions_for_app: forward_to_program_container, + load_actions: forward_to_program_container, + create_default_tab, + show_collapsed_tab_group, + hide_collapsed_tab_group, + tile_focus_changed + } + return function onmessage (msg) { + console.error('tab_group: message from root', msg.type) + ;(on[msg.type] || onfail)(msg) + } + function update_quick_actions_for_app (msg) { + console.error('tab_group: forwarding to action_bar', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.(msg.type, refs, msg.data) + } + function update_steps_wizard_for_app (msg) { + console.error('tab_group: forwarding to action_executor', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.action_executor?.(msg.type, refs, msg.data) + } + function forward_to_program_container (msg) { + console.error('tab_group: forwarding to program_container', msg.type) + const refs = msg.head ? { cause: msg.head } : {} + _.program_container?.(msg.type, refs, msg.data) + } + function create_default_tab (msg) { + console.error('tab_group: creating default tab', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('add_default_tab', refs, msg.data) + } + function show_collapsed_tab_group (msg) { + console.error('tab_group: showing collapsed tab group', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('show_collapsed_tab_group', refs, msg.data) + } + function hide_collapsed_tab_group (msg) { + console.error('tab_group: hiding collapsed tab group', msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('hide_collapsed_tab_group', refs, msg.data) + } + function tile_focus_changed (msg) { + console.error('tab_group: tile focus changed', msg.data) + const { is_focused } = msg.data + tab_group_container.classList.toggle('tile-focused', is_focused) + // Forward to tabs so active tab styling can update + const refs = msg.head ? { cause: msg.head } : {} + _.tabs?.('tile_focus_changed', refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unknown root message', msg) } + } + + // --------------------------- + // PROTOCOLS + // --------------------------- + + function io_tabs () { + return function onmessage (msg) { + console.error('tab_group: tabs_protocol', msg.type, msg.data) + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + } + + function io_program_container () { + const forward_up = { + ui_focus: forward, + action_auto_completed: forward, + action_complete: forward + } + return function onmessage (msg) { + console.error('tab_group: program_container_protocol', msg.type) + ;(forward_up[msg.type] || onfail)(msg) + } + function forward (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled program_container msg', msg) } + } + + function io_action_bar () { + const forward_to_executor = { + action_submitted: to_executor, + selected_action: to_executor, + activate_steps_wizard: to_executor, + render_form: to_executor, + clean_up: to_executor + } + const forward_up = { + ui_focus: to_up, + display_actions: to_up, + filter_actions: to_up, + console_history_toggle: to_up + } + return function onmessage (msg) { + console.error('tab_group: action_bar_protocol', msg.type) + const handler = forward_to_executor[msg.type] || forward_up[msg.type] || onfail + handler(msg) + } + function to_executor (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_executor?.(msg.type, refs, msg.data) + } + function to_up (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled action_bar msg', msg) } + } + + function io_action_executor () { + const forward_to_action_bar = { + selected_action: to_action_bar, + show_submit_btn: to_action_bar, + hide_submit_btn: to_action_bar, + update_quick_actions_input: to_action_bar, + step_clicked: to_action_bar, + load_actions: to_action_bar + } + const forward_up = { + action_auto_completed: to_up_and_bar, + action_complete: to_up + } + return function onmessage (msg) { + console.error('tab_group: action_executor_protocol', msg.type) + const handler = forward_to_action_bar[msg.type] || forward_up[msg.type] || onfail + handler(msg) + } + function to_action_bar (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.(msg.type, refs, msg.data) + } + function to_up (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.up(msg.type, refs, msg.data) + } + function to_up_and_bar (msg) { + const refs = msg.head ? { cause: msg.head } : {} + _.action_bar?.('action_submitted', refs, msg.data) + _.up(msg.type, refs, msg.data) + } + function onfail (msg) { console.error('tab_group: unhandled action_executor msg', msg) } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + tabs: { $: '' }, + program_container: { $: '' }, + action_bar: { $: '' }, + action_executor: { $: '' }, + net_helper: { $: '' } + } + } + + function fallback_instance () { + return { + _: { + program_container: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs_style: 'docs_style', + docs: 'docs' + } + }, + action_executor: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + data: 'data', + hardcons: 'hardcons', + variables: 'variables' + } + }, + tabs: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + docs: 'docs', + variables: 'variables' + } + }, + action_bar: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + hardcons: 'hardcons', + prefs: 'prefs', + variables: 'variables', + data: 'data' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'tab_group.css': { + $ref: 'style/tab_group.css' + } + } + } + } + } +} diff --git a/src/node_modules/tabbed_editor/README.md b/src/node_modules/tabbed_editor/README.md new file mode 100644 index 0000000..6076829 --- /dev/null +++ b/src/node_modules/tabbed_editor/README.md @@ -0,0 +1 @@ +A code editor container that manages multiple file tabs with syntax highlighting, supporting switch, close, and toggle tab operations. diff --git a/src/node_modules/tabbed_editor/package.json b/src/node_modules/tabbed_editor/package.json new file mode 100644 index 0000000..241dc32 --- /dev/null +++ b/src/node_modules/tabbed_editor/package.json @@ -0,0 +1,3 @@ +{ + "main": "tabbed_editor.js" +} \ No newline at end of file diff --git a/src/node_modules/tabbed_editor/tabbed_editor.js b/src/node_modules/tabbed_editor/tabbed_editor.js new file mode 100644 index 0000000..1c3d882 --- /dev/null +++ b/src/node_modules/tabbed_editor/tabbed_editor.js @@ -0,0 +1,407 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +// const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = tabbed_editor + +async function tabbed_editor (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + files: onfiles, + active_tab: onactivetab + } + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
Select a file to edit
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const editor_content = shadow.querySelector('.editor-content') + + let init = false + let files = {} + let active_tab = null + let current_editor = null + const on_message = { + switch_tab: handle_switch_tab, + close_tab: handle_close_tab, + toggle_tab: handle_toggle_tab + } + + const { io, _ } = net(id) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function handle_switch_tab (msg) { switch_to_tab(msg.data, msg) } + function handle_close_tab (msg) { + const tab_data = msg.data + if (active_tab === tab_data.id) { + hide_editor() + active_tab = null + } + + _.up('tab_closed', msg.head ? { cause: msg.head } : {}, tab_data) + } + function handle_toggle_tab (msg) { + const tab_data = msg.data + if (active_tab === tab_data.id) { + hide_editor() + active_tab = null + } else { + switch_to_tab(tab_data, msg) + } + } + function onmessage_fail () { /* docs_toggle @TODO */ } + + function switch_to_tab (tab_data, msg) { + if (active_tab === tab_data.id) { + return + } + + active_tab = tab_data.id + create_editor(tab_data) + _.up('tab_switched', msg?.head ? { cause: msg.head } : {}, tab_data) + } + + function create_editor (tab_data) { + const parsed_data = JSON.parse(tab_data[0]) + const file_content = files[parsed_data.id] || '' + // console.log('Creating editor for:', parsed_data) + + editor_content.replaceChildren() + + editor_content.innerHTML = ` +
+
+
+ +
+
` + const editor = editor_content.querySelector('.code-editor') + const line_numbers = editor_content.querySelector('.line-numbers') + const code_area = editor_content.querySelector('.code-area') + current_editor = { editor, code_area, line_numbers, tab_data: parsed_data } + + code_area.oninput = handle_code_input + code_area.onscroll = handle_code_scroll + + update_line_numbers() + } + + function hide_editor () { + editor_content.innerHTML = ` +
+
Select a file to edit
+
` + current_editor = null + } + + function update_line_numbers () { + if (!current_editor) return + + const { code_area, line_numbers } = current_editor + const lines = code_area.value.split('\n') + const line_count = lines.length + + let line_html = '' + for (let i = 1; i <= line_count; i++) { + line_html += `
${i}
` + } + + line_numbers.innerHTML = line_html + } + + function save_file_content () { + if (!current_editor) return + + const { code_area, tab_data } = current_editor + files[tab_data.id] = code_area.value + _.up('file_changed', {}, { + id: tab_data.id, + content: code_area.value + }) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + init = true + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('Invalid message', { data, type }) } + function inject (data) { sheet.replaceSync(data[0]) } + function onfiles (data) { files = data[0] } + + function onactivetab (data) { + if (data.id !== active_tab) { + switch_to_tab(data) + } + } + + function handle_code_input () { + update_line_numbers() + save_file_content() + } + + function handle_code_scroll () { + if (!current_editor) return + const { code_area, line_numbers } = current_editor + line_numbers.scrollTop = code_area.scrollTop + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + net_helper: { + $: '' + } + // DOCS: { + // $: '' + // }, + } + } + + function fallback_instance () { + return { + _: { + net_helper: { + 0: '' + } + // DOCS: { + // 0: '' + // }, + }, + drive: { + 'files/': { + 'example.js': { + raw: ` + function hello() { + console.log("Hello, World!"); + } + + const x = 42; + let y = "string"; + + if (x > 0) { + hello(); + } + ` + }, + 'example.md': { + raw: ` + # Example Markdown + This is an **example** markdown file. + + ## Features + + - Syntax highlighting + - Line numbers + - File editing + + \`\`\`javascript + function example() { + return true; + } + \`\`\` + ` + }, + 'data.json': { + raw: ` + { + "name": "example", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.21" + } + ` + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + raw: ` + .tabbed-editor { + width: 100%; + height: 100%; + min-height: 80px; + background-color: #0d1117; + color: #e6edf3; + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; + display: grid; + grid-template-rows: 1fr; + position: relative; + border: 1px solid #30363d; + border-radius: 6px; + box-sizing: border-box; + overflow: hidden; + } + + .editor-content { + display: grid; + grid-template-rows: 1fr; + min-height: 0; + position: relative; + overflow: hidden; + background-color: #0d1117; + } + + .editor-placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #7d8590; + font-style: italic; + font-size: 16px; + background: linear-gradient(135deg, #0d1117 0%, #161b22 100%); + } + + .code-editor { + height: 100%; + min-height: 0; + display: grid; + grid-template-rows: 1fr; + background-color: #0d1117; + } + + .editor-wrapper { + display: grid; + grid-template-columns: auto 1fr; + min-height: 0; + height: 100%; + position: relative; + box-sizing: border-box; + overflow: auto; + scrollbar-gutter: stable; + scrollbar-width: thin; + scrollbar-color: #30363d transparent; + background-color: #0d1117; + } + + .line-numbers { + background-color: #161b22; + color: #7d8590; + padding: 12px 16px; + text-align: right; + user-select: none; + font-size: 13px; + line-height: 20px; + font-weight: 400; + border-right: 1px solid #21262d; + position: sticky; + left: 0; + z-index: 1; + height: 100%; + } + + .line-number { + height: 20px; + line-height: 20px; + transition: color 0.1s ease; + } + + .line-number:hover { + color: #f0f6fc; + } + + .code-area { + background-color: #0d1117; + color: #e6edf3; + border: none; + outline: none; + resize: none; + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; + font-size: 13px; + line-height: 20px; + padding: 12px 16px; + position: relative; + z-index: 2; + tab-size: 2; + white-space: pre; + overflow-wrap: normal; + overflow-x: auto; + } + + .code-area:focus { + background-color: #0d1117; + box-shadow: none; + } + + .code-area::selection { + background-color: #264f78; + } + + .editor-wrapper::-webkit-scrollbar { + width: 10px; + height: 10px; + } + + .editor-wrapper::-webkit-scrollbar-track { + background: transparent; + } + + .editor-wrapper::-webkit-scrollbar-thumb { + background: #30363d; + border: 2px solid transparent; + border-radius: 999px; + background-clip: content-box; + } + + .editor-wrapper::-webkit-scrollbar-thumb:hover { + background: #484f58; + border: 2px solid transparent; + background-clip: content-box; + } + ` + } + }, + 'active_tab/': { + 'current.json': { + raw: JSON.stringify({ + id: 'example.js', + name: 'example.js' + }) + } + } + } + } + } +} diff --git a/src/node_modules/tabs/README.md b/src/node_modules/tabs/README.md new file mode 100644 index 0000000..4cdddcd --- /dev/null +++ b/src/node_modules/tabs/README.md @@ -0,0 +1 @@ +A horizontal scrollable tab bar component with drag-to-scroll support that displays tab entries with icons and close buttons. diff --git a/src/node_modules/tabs/arrow-left.svg b/src/node_modules/tabs/arrow-left.svg new file mode 100644 index 0000000..98b4647 --- /dev/null +++ b/src/node_modules/tabs/arrow-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/node_modules/tabs/arrow-right.svg b/src/node_modules/tabs/arrow-right.svg new file mode 100644 index 0000000..1446566 --- /dev/null +++ b/src/node_modules/tabs/arrow-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/node_modules/tabs/cross.svg b/src/node_modules/tabs/cross.svg new file mode 100644 index 0000000..d836234 --- /dev/null +++ b/src/node_modules/tabs/cross.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/node_modules/tabs/icon.svg b/src/node_modules/tabs/icon.svg new file mode 100644 index 0000000..e6c11ea --- /dev/null +++ b/src/node_modules/tabs/icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/node_modules/tabs/package.json b/src/node_modules/tabs/package.json new file mode 100644 index 0000000..97d9049 --- /dev/null +++ b/src/node_modules/tabs/package.json @@ -0,0 +1,3 @@ +{ + "main": "tabs.js" +} \ No newline at end of file diff --git a/src/node_modules/tabs/separator.svg b/src/node_modules/tabs/separator.svg new file mode 100644 index 0000000..57f03f2 --- /dev/null +++ b/src/node_modules/tabs/separator.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/node_modules/tabs/style.css b/src/node_modules/tabs/style.css new file mode 100644 index 0000000..2a6c10a --- /dev/null +++ b/src/node_modules/tabs/style.css @@ -0,0 +1,228 @@ +.first-half { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; +} +.tab-entries { + display: flex; + flex-direction: row; + flex:auto; + justify-content: flex-start; + align-items: center; + align-content: center; + flex-wrap: nowrap; + overflow-x: hidden; + background-color: #131315; + column-gap: 14px; + padding: 10px 2px; +} +.tabsbtn { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + background-color: #191919; + padding: 8px 14px; + border-radius: 30px; +} +.icon { + margin-right: 5px; + display: flex; +} +.name { + display: flex; + align-items: center; + font-size: 14px; + margin-right: 5px; + user-select: none; + color: #a6a6a6; + white-space: nowrap; +} +.btn { + border: none; + display: flex; + padding: 0; + background-color: transparent; + color: #ffffff; +} +svg.btn{ + width: 25px; + height: 25px; +} +svg{ + width: 20px; + height: 20px; +} +.tab-entries.tile-focused .tabsbtn.active { + background: rgba(103, 195, 255, 0.25); + border-color: transparent; +} +.tab-entries.tile-inactive .tabsbtn.active { + background: transparent; + border: 1px solid rgba(103, 195, 255, 0.5); +} +.tab-group-inline { + display: inline-flex; + align-items: stretch; + height: 100%; +} +.tab-group-inline.expanded { + border-bottom: 2px solid rgba(103, 195, 255, 0.8); +} +.tab-group-inline .tab-group-tab { + cursor: pointer; +} +.tab-group-inline .tab-group-tab .icon { + display: inline-flex; + align-items: center; + color: rgba(103, 195, 255, 0.8); +} +.tab-group-inline .tab-group-tab .icon svg { + width: 12px; + height: 12px; +} +.tab-group-children { + display: none; + align-items: stretch; +} +.tab-group-inline.expanded .tab-group-children { + display: inline-flex; +} +.tab-group-child { + cursor: pointer; +} +.tab-group-child .icon { + display: inline-flex; + align-items: center; + color: rgba(103, 195, 255, 0.6); +} +.tab-group-child .icon svg { + width: 12px; + height: 12px; +} + +.nested-collapse-container { + display: inline-flex; + align-items: center; + gap: 8px; + height: 100%; +} + +.nested-group-container { + display: inline-flex; + align-items: center; + gap: 6px; + border-radius: 20px; +} + +.nested-group-leaf { + display: inline-flex; + align-items: center; + gap: 6px; +} + +.nested-leaf-tab { + cursor: pointer; + transition: all 0.2s ease; + border: 1px solid transparent; + display: inline-flex; + align-items: center; + gap: 6px; +} + +.nested-leaf-tab:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(255, 255, 255, 0.12); +} + +.nested-leaf-tab .name { + color: #a6a6a6; +} + +.nested-leaf-tab.active .name { + color: #ffffff; +} + +.nested-leaf-tab.depth-1 { + padding: 6px 12px; +} +.nested-leaf-tab.depth-1 .name { + font-size: 13px; +} + +.nested-leaf-tab.depth-2 { + padding: 5px 10px; +} +.nested-leaf-tab.depth-2 .name { + font-size: 12px; +} + +.nested-leaf-tab.depth-3 { + padding: 4px 8px; +} +.nested-leaf-tab.depth-3 .name { + font-size: 11px; +} + +.group-arrow-btn { + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + opacity: 0.7; + transition: opacity 0.2s ease, transform 0.2s ease; + margin-right: 4px; + color: #a6a6a6; +} + +.group-arrow-btn:hover { + opacity: 1; + transform: scale(1.1); +} + +.group-arrow-btn svg { + width: 12px; + height: 12px; + display: block; +} + +.nested-leaf-tab .icon { + display: inline-flex; + align-items: center; +} + +.nested-leaf-tab.depth-1 .icon { + opacity: 0.8; +} +.nested-leaf-tab.depth-2 .icon { + opacity: 0.65; +} +.nested-leaf-tab.depth-3 .icon { + opacity: 0.5; +} +.nested-leaf-tab.depth-1 .icon svg, +.nested-leaf-tab.depth-2 .icon svg, +.nested-leaf-tab.depth-3 .icon svg { + width: 14px; + height: 14px; +} + +.nested-collapse-container > .nested-group-container.expanded { + border-bottom: 2px solid #FF8D28; + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + padding-bottom: 6px; +} + +.tab-separator { + display: inline-flex; + align-items: center; + justify-content: center; + align-self: center; + padding: 0; + margin: 0 6px; + width: 2px; + height: 24px; + flex-shrink: 0; +} \ No newline at end of file diff --git a/src/node_modules/tabs/tabs.js b/src/node_modules/tabs/tabs.js new file mode 100644 index 0000000..7585566 --- /dev/null +++ b/src/node_modules/tabs/tabs.js @@ -0,0 +1,748 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = component + +async function component (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + variables: onvariables, + style: inject, + icons: iconject, + scroll: onscroll + } + const div = document.createElement('div') + const shadow = div.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
` + const entries = shadow.querySelector('.tab-entries') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + let init = false + let variables = [] + let dricons = [] + let active = null + let tab_group_el = null + let nested_collapse_el = null + const expanded_groups = {} + let current_collapse_data = null + let tab_group_expanded = false + let tile_is_focused = false + let docs_action_id = 0 + const default_tabs = {} + const link_tabs = {} + const variable_tabs = {} + let ARROW_LEFT_SVG = '' + let ARROW_RIGHT_SVG = '' + let SEPARATOR_SVG = '' + const docs = DOCS(__filename)(opts.sid) + const { io, _ } = net(id) + + const actions_file = await drive.get('actions/commands.json') + const docs_actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + docs.register_actions(docs_actions) + + await sdb.watch(onbatch) + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + if (entries) { + let is_down = false + let start_x + let scroll_start + + function stop_drag_scroll () { + is_down = false + entries.classList.remove('grabbing') + update_scroll_position() + } + + function move_drag_scroll (pointer_x) { + if (!is_down) return + if (entries.scrollWidth <= entries.clientWidth) return stop_drag_scroll() + entries.scrollLeft = scroll_start - (pointer_x - start_x) * 1.5 + } + + entries.onmousedown = on_entries_mousedown + + function on_entries_mousedown (e) { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + entries.classList.add('grabbing') + start_x = e.pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + window.onmousemove = on_window_mousemove + window.onmouseup = on_window_mouseup + } + + function on_window_mousemove (e) { + move_drag_scroll(e.pageX - entries.offsetLeft) + e.preventDefault() + } + + function on_window_mouseup () { + stop_drag_scroll() + window.onmousemove = null + window.onmouseup = null + } + + entries.onmouseleave = stop_drag_scroll + entries.ontouchstart = on_entries_touchstart + + function on_entries_touchstart (e) { + if (entries.scrollWidth <= entries.clientWidth) return + is_down = true + start_x = e.touches[0].pageX - entries.offsetLeft + scroll_start = entries.scrollLeft + } + + ;['ontouchend', 'ontouchcancel'].forEach(bind_touch_end_handler) + entries.ontouchmove = on_entries_touchmove + + function bind_touch_end_handler (event_name) { entries[event_name] = stop_drag_scroll } + function on_entries_touchmove (e) { + move_drag_scroll(e.touches[0].pageX - entries.offsetLeft) + e.preventDefault() + } + } + return div + + function io_up () { + const on = { + add_link_tab: handle_add_link_tab, + remove_link_tab: handle_remove_link_tab, + add_default_tab: handle_add_default_tab, + restore_tab: handle_restore_tab, + show_collapsed_tab_group: handle_show_collapsed_tab_group, + hide_collapsed_tab_group: handle_hide_collapsed_tab_group, + tile_focus_changed: handle_tile_focus_changed, + sync_tab_count: handle_sync_tab_count + } + return function onmessage (msg) { + console.error('tabs: message from up', msg) + ;(on[msg.type] || onfail)(msg) + } + function handle_add_link_tab ({ data }) { add_link_tab(data) } + function handle_remove_link_tab ({ data }) { remove_link_tab(data) } + function handle_add_default_tab ({ data }) { add_default_tab(data) } + function handle_restore_tab ({ data }) { + create_btn({ name: data.name, id: data.id }) + sync_tab_count() + } + function handle_show_collapsed_tab_group ({ data }) { show_collapsed_tab_group(data) } + function handle_hide_collapsed_tab_group () { hide_collapsed_tab_group() } + function handle_tile_focus_changed ({ data }) { update_tab_focus_state(data) } + function handle_sync_tab_count () { sync_tab_count() } + function onfail (msg) { console.error('tabs: unknown message', msg) } + } + + function add_default_tab ({ name, program, tile_id, id }) { + const tab_id = id || 'tab_' + Date.now() + if (default_tabs[tab_id]) { + console.error('tabs: default tab already exists', tab_id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + ${dricons[1] || '📄'} + ${tab_id} + ${name || 'New Tab'} + ` + el.className = 'tabsbtn default-tab active' + const name_el = el.querySelector('.name') + const close_btn = el.querySelector('.btn') + default_tabs[tab_id] = { el, name_el, close_btn, name, program } + if (active) active.classList.remove('active') + active = el + name_el.onclick = switch_active + close_btn.onclick = close_tab + entries.appendChild(el) + console.error('tabs: default tab added', tab_id) + sync_tab_count() + sync_separators() + + function switch_active () { + console.error('tabs: default tab clicked', tab_id) + if (active) active.classList.remove('active') + el.classList.add('active') + active = el + const data = { id: tab_id, name, program } + _.up('ui_focus', {}, { type: 'tab', sid: opts.sid }) + _.up('tab_name_clicked', {}, data) + } + function close_tab (e) { + e.stopPropagation() + console.error('tabs: default tab close clicked', tab_id) + el.remove() + delete default_tabs[tab_id] + if (active === el) active = null + _.up('tab_close_clicked', {}, { id: tab_id, name }) + sync_tab_count() + if (Object.keys(default_tabs).length === 0) { + _.up('all_tabs_closed', {}, null) + } + sync_separators() + } + } + + function add_link_tab ({ tile_id, name, direction }) { + const link_tab_id = 'split_tile_' + tile_id + if (link_tabs[link_tab_id]) { + console.error('tabs: link tab already exists', link_tab_id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + + ${name || 'Split ' + direction}` + el.className = 'tabsbtn link-tab' + const name_el = el.querySelector('.name') + link_tabs[link_tab_id] = { el, name_el, tile_id, name, direction } + el.onclick = on_link_tab_click + entries.appendChild(el) + console.error('tabs: link tab added', link_tab_id) + sync_separators() + + function on_link_tab_click () { + console.error('tabs: link tab clicked', link_tab_id) + _.up('link_tab_clicked', {}, { tile_id, link_tab_id }) + } + } + + function remove_link_tab ({ tile_id }) { + const link_tab_id = 'split_tile_' + tile_id + const tab = link_tabs[link_tab_id] + if (tab) { + tab.el.remove() + delete link_tabs[link_tab_id] + console.error('tabs: link tab removed', link_tab_id) + sync_separators() + } + } + + function show_collapsed_tab_group (data) { + if (data.nested) { + show_nested_collapse(data) + return + } + + const { tiles } = data + if (tab_group_el) hide_collapsed_tab_group() + + tab_group_el = document.createElement('div') + tab_group_el.className = 'tab-group-inline' + + const group_tab = document.createElement('div') + group_tab.className = 'tabsbtn tab-group-tab' + const direction = (tiles[0] && tiles[0].direction) || 'right' + const icon_svg = (direction === 'left' || direction === 'up') ? ARROW_LEFT_SVG : ARROW_RIGHT_SVG + group_tab.innerHTML = `${icon_svg}Split` + group_tab.onclick = toggle_tab_group_expand + tab_group_el.appendChild(group_tab) + + const children_container = document.createElement('div') + children_container.className = 'tab-group-children' + + for (const tile of tiles) { + const tile_tab_list = tile.tabs || [] + for (const tab of tile_tab_list) { + const tab_el = document.createElement('div') + tab_el.className = 'tabsbtn tab-group-child' + tab_el.innerHTML = `${dricons[1] || '📄'}${tab.name || 'Tab'}` + tab_el.onclick = on_child_tab_click(tile.tile_id) + children_container.appendChild(tab_el) + } + if (tile_tab_list.length === 0) { + const placeholder_el = document.createElement('div') + placeholder_el.className = 'tabsbtn tab-group-child' + placeholder_el.innerHTML = `${dricons[1] || '📄'}New Tab` + placeholder_el.onclick = on_child_tab_click(tile.tile_id) + children_container.appendChild(placeholder_el) + } + } + + tab_group_el.appendChild(children_container) + entries.insertBefore(tab_group_el, entries.firstChild) + console.error('tabs: collapsed tab group shown') + sync_separators() + + function toggle_tab_group_expand () { + tab_group_expanded = !tab_group_expanded + tab_group_el.classList.toggle('expanded', tab_group_expanded) + sync_separators() + } + + function on_child_tab_click (tile_id) { + return function () { + console.error('tabs: tab group child clicked, requesting expand', tile_id) + _.up('tab_group_tile_clicked', {}, { tile_id }) + } + } + } + + function hide_collapsed_tab_group () { + if (tab_group_el) { + tab_group_el.remove() + tab_group_el = null + tab_group_expanded = false + console.error('tabs: collapsed tab group hidden') + } + if (nested_collapse_el) { + nested_collapse_el.remove() + nested_collapse_el = null + console.error('tabs: nested collapse hidden') + } + sync_separators() + } + + function show_nested_collapse (data) { + console.error('tabs: show_nested_collapse', data) + current_collapse_data = data + if (tab_group_el) { + tab_group_el.remove() + tab_group_el = null + } + if (nested_collapse_el) { + nested_collapse_el.remove() + nested_collapse_el = null + } + + const { collapsed_groups } = data + + if (!collapsed_groups || collapsed_groups.length === 0) return + + nested_collapse_el = document.createElement('div') + nested_collapse_el.className = 'nested-collapse-container' + + function get_first_leaf_node (node) { + if (!node) return null + if (node.type === 'leaf') return node + if (node.children && node.children.length > 0) { + return get_first_leaf_node(node.children[0]) + } + return null + } + + // `depth` is the nesting depth in the collapsed strip, it drives the size ramp in CSS. + function render_leaf_tab (lt, leaf, depth, is_header = false, is_expanded = false, parent_group = null) { + const el = document.createElement('div') + el.className = `tabsbtn nested-leaf-tab depth-${Math.min(depth + 1, 3)}` + if (active && active.querySelector('.name')?.textContent === (lt.name || 'Tab')) { + el.classList.add('active') + } + + let arrow_btn_html = '' + if (is_header && parent_group) { + const icon_svg = is_expanded ? ARROW_LEFT_SVG : ARROW_RIGHT_SVG + arrow_btn_html = `${icon_svg}` + } + + // A folded tab is a pointer to another tile, not an owned tab, so it has no close + // button: closing stays with the tile that owns the tab. + el.innerHTML = ` + ${arrow_btn_html} + ${dricons[1] || '📄'} + ${lt.name || 'Tab'}` + + if (is_header && parent_group) { + const arrow_btn = el.querySelector('.group-arrow-btn') + if (arrow_btn) { + arrow_btn.onclick = (e) => { + e.stopPropagation() + expanded_groups[parent_group.split_id] = !is_expanded + show_nested_collapse(current_collapse_data) + } + } + } + + const name_el = el.querySelector('.name') + if (name_el) { + name_el.onclick = on_folded_tab_click + } + + return el + + // Clicking a folded tab asks the tile manager to bring that tile back on screen. + function on_folded_tab_click () { + if (active) active.classList.remove('active') + el.classList.add('active') + active = el + _.up('ui_focus', {}, { type: 'tab', sid: opts.sid }) + _.up('tab_group_tile_clicked', {}, { tile_id: leaf.tile_id, id: lt.id, name: lt.name }) + } + } + + function render_node_to_dom (node, depth, is_header_child = false, parent_group = null) { + if (!node) return null + + if (node.type === 'leaf') { + const leaf_container = document.createElement('div') + leaf_container.className = 'nested-group-leaf' + leaf_container.setAttribute('data-depth', depth) + + let leaf_tabs = node.tabs || [] + if (leaf_tabs.length === 0) { + leaf_tabs = [{ id: 'placeholder', name: 'New Tab', tile_id: node.tile_id }] + } + + if (is_header_child && parent_group) { + const is_expanded = !!expanded_groups[parent_group.split_id] + const header_tab = render_leaf_tab(leaf_tabs[0], node, depth, true, is_expanded, parent_group) + leaf_container.appendChild(header_tab) + + if (is_expanded) { + for (let i = 1; i < leaf_tabs.length; i++) { + const normal_tab = render_leaf_tab(leaf_tabs[i], node, depth, false, false, null) + leaf_container.appendChild(normal_tab) + } + } + } else { + for (const lt of leaf_tabs) { + const normal_tab = render_leaf_tab(lt, node, depth, false, false, null) + leaf_container.appendChild(normal_tab) + } + } + + return leaf_container + } + + if (node.type === 'collapsed_group' || node.type === 'split_group') { + const is_expanded = !!expanded_groups[node.split_id] + + if (!is_expanded) { + const first_leaf = get_first_leaf_node(node) + if (!first_leaf) return null + + let leaf_tabs = first_leaf.tabs || [] + if (leaf_tabs.length === 0) { + leaf_tabs = [{ id: 'placeholder', name: 'New Tab', tile_id: first_leaf.tile_id }] + } + + const leaf_container = document.createElement('div') + leaf_container.className = 'nested-group-leaf' + leaf_container.setAttribute('data-depth', depth) + + const header_tab = render_leaf_tab(leaf_tabs[0], first_leaf, depth, true, false, node) + leaf_container.appendChild(header_tab) + return leaf_container + } else { + const group_container = document.createElement('div') + group_container.className = `nested-group-container depth-${depth} expanded` + group_container.setAttribute('data-depth', depth) + + if (node.children && node.children.length > 0) { + const first_child_el = render_node_to_dom(node.children[0], depth, true, node) + if (first_child_el) group_container.appendChild(first_child_el) + + for (let i = 1; i < node.children.length; i++) { + const child_el = render_node_to_dom(node.children[i], depth + 1, false, null) + if (child_el) group_container.appendChild(child_el) + } + } + + return group_container + } + } + + return null + } + + for (const group of collapsed_groups) { + const group_el = render_node_to_dom(group, 0, false, null) + if (group_el) nested_collapse_el.appendChild(group_el) + } + + entries.insertBefore(nested_collapse_el, entries.firstChild) + console.error('tabs: nested collapse shown with', collapsed_groups.length, 'groups') + sync_separators() + } + + function update_tab_focus_state ({ is_focused }) { + entries.classList.toggle('tile-focused', is_focused) + entries.classList.toggle('tile-inactive', !is_focused) + } + + function get_tab_count () { + return Object.keys(default_tabs).length + Object.keys(variable_tabs).length + } + + function sync_tab_count () { + _.up('update_tab_count', {}, { count: get_tab_count() }) + } + + async function create_btn ({ name, id }, index = 0) { + if (variable_tabs[id]) { + console.error('tabs: variable tab already exists', id) + return + } + const el = document.createElement('div') + el.innerHTML = ` + ${dricons[index + 1] || dricons[1] || '📄'} + ${id} + ${name} + ` + + el.className = 'tabsbtn' + const name_el = el.querySelector('.name') + const close_btn = el.querySelector('.btn') + + name_el.draggable = false + + const action_id = ++docs_action_id + const open_action = create_action('Open Tab ' + action_id, 'Open the ' + name + ' tab.', 'tab', on_tab_name_click) + const close_action = create_action('Close Tab ' + action_id, 'Close the ' + name + ' tab.', 'close', on_tab_close_click) + on_name_click.info = open_action.info + on_name_click.opts = { state: { action: open_action.name } } + on_close_click.info = close_action.info + on_close_click.opts = { state: { action: close_action.name } } + name_el.onclick = docs.wrap_isolated(on_name_click) + close_btn.onclick = docs.wrap_isolated(on_close_click) + variable_tabs[id] = { el, name, actions: [open_action, close_action] } + register_actions() + + function on_name_click (event, $) { $($.state.action) } + function on_close_click (event, $) { + event.stopPropagation() + $($.state.action) + } + function on_tab_name_click () { + const data = { type: 'tab', sid: opts.sid } + _.up('ui_focus', {}, data) + _.up('tab_name_clicked', {}, { id, name }) + } + function on_tab_close_click () { + el.remove() + delete variable_tabs[id] + register_actions() + const data = { type: 'tab', sid: opts.sid } + _.up('ui_focus', {}, data) + _.up('tab_close_clicked', {}, { id, name }) + sync_tab_count() + } + entries.appendChild(el) + sync_separators() + } + + function create_action (name, info, icon, run) { + return { name, info, icon, status: { hidden: true }, steps: [], run } + } + + function register_actions () { + const tab_actions = Object.values(variable_tabs).flatMap(tab => tab.actions) + docs.register_actions(docs_actions.concat(tab_actions)) + } + + function create_separator () { + const sep = document.createElement('div') + sep.className = 'tab-separator' + sep.innerHTML = ` + + + + ` + return sep + } + + function sync_separators () { + const existing_seps = entries.querySelectorAll(':scope > .tab-separator') + for (const sep of existing_seps) { + sep.remove() + } + + const main_children = Array.from(entries.children).filter(child => { + if (child.style.display === 'none') return false + return child.classList.contains('tabsbtn') || + child.classList.contains('nested-collapse-container') || + child.classList.contains('tab-group-inline') + }) + + for (let i = 0; i < main_children.length - 1; i++) { + const sep = create_separator() + entries.insertBefore(sep, main_children[i].nextSibling) + } + + if (nested_collapse_el) { + const group_children = Array.from(nested_collapse_el.children).filter(child => { + if (child.style.display === 'none') return false + return child.classList.contains('nested-group-container') || + child.classList.contains('nested-group-leaf') + }) + + const existing_nested_seps = nested_collapse_el.querySelectorAll(':scope > .tab-separator') + for (const sep of existing_nested_seps) { + sep.remove() + } + + for (let i = 0; i < group_children.length - 1; i++) { + const sep = create_separator() + nested_collapse_el.insertBefore(sep, group_children[i].nextSibling) + } + } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + if (!init) { + if (!opts.ids) variables.forEach(create_btn) + init = true + } else { + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + + function onvariables (data) { + const vars = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + variables = vars + } + + function iconject (data) { + dricons = data + ARROW_LEFT_SVG = data[4] || '' + ARROW_RIGHT_SVG = data[5] || '' + SEPARATOR_SVG = data[6] || '' + } + + function update_scroll_position () { + } + + function onscroll (data) { + setTimeout(apply_scroll_position, 200) + function apply_scroll_position () { + if (entries) { + entries.scrollLeft = data + } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'icons/': { + 'cross.svg': { + $ref: 'cross.svg' + }, + '1.svg': { + $ref: 'icon.svg' + }, + '2.svg': { + $ref: 'icon.svg' + }, + '3.svg': { + $ref: 'icon.svg' + }, + '4.svg': { + $ref: 'arrow-left.svg' + }, + '5.svg': { + $ref: 'arrow-right.svg' + }, + '6.svg': { + $ref: 'separator.svg' + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'New Tab', + info: 'Create a new tab in the current tab strip.', + icon: 'plus', + status: { + pinned: true, + default: true + }, + steps: [ + { name: 'Enter Tab Name', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Duplicate Tab', + info: 'Copy an existing tab and open the duplicate as a new tab.', + icon: 'copy', + status: { + pinned: false, + default: false + }, + steps: [ + { name: 'Select Tab to Duplicate', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Enter New Tab Name', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Close Tab', + info: 'Close the selected tab after confirmation.', + icon: 'close', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Tab to Close', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Close', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'variables/': { + 'tabs.json': { + $ref: 'tabs.json' + } + }, + 'scroll/': { + 'position.json': { + raw: '100' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + }, + 'style/': { + 'theme.css': { + $ref: 'style.css' + } + } + } + } + } +} diff --git a/src/node_modules/tabs/tabs.json b/src/node_modules/tabs/tabs.json new file mode 100644 index 0000000..7135196 --- /dev/null +++ b/src/node_modules/tabs/tabs.json @@ -0,0 +1,5 @@ +[ + { "name": "example.js", "id": "0 :" }, + { "name": "example.md", "id": "2 :" }, + { "name": "data.json", "id": "1 :" } +] \ No newline at end of file diff --git a/src/node_modules/tabsbar/README.md b/src/node_modules/tabsbar/README.md new file mode 100644 index 0000000..143a9a9 --- /dev/null +++ b/src/node_modules/tabsbar/README.md @@ -0,0 +1 @@ +A top navigation bar containing the wizard hat button, tabs component, task_manager, and help button for global application controls. diff --git a/src/node_modules/tabsbar/docs.svg b/src/node_modules/tabsbar/docs.svg new file mode 100644 index 0000000..3330095 --- /dev/null +++ b/src/node_modules/tabsbar/docs.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/node_modules/tabsbar/hat.svg b/src/node_modules/tabsbar/hat.svg new file mode 100644 index 0000000..e6c11ea --- /dev/null +++ b/src/node_modules/tabsbar/hat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/node_modules/tabsbar/package.json b/src/node_modules/tabsbar/package.json new file mode 100644 index 0000000..f2e6a7f --- /dev/null +++ b/src/node_modules/tabsbar/package.json @@ -0,0 +1,3 @@ +{ + "main": "tabsbar.js" +} \ No newline at end of file diff --git a/src/node_modules/tabsbar/tabsbar.js b/src/node_modules/tabsbar/tabsbar.js new file mode 100644 index 0000000..b0b20fd --- /dev/null +++ b/src/node_modules/tabsbar/tabsbar.js @@ -0,0 +1,454 @@ +const STATE = require('STATE') +const state_db = STATE(__filename) +const { get } = state_db(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +const tabs_component = require('tabs') +const task_manager = require('task_manager') + +module.exports = tabsbar + +async function tabsbar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + icons: inject_icons + } + + let dricons = {} + let docs_toggle_active = false + const on_message = { + docs_toggle: handle_docs_toggle, + add_link_tab: handle_forward_tabs, + remove_link_tab: handle_forward_tabs, + show_collapsed_tab_group: handle_forward_tabs, + hide_collapsed_tab_group: handle_forward_tabs, + tile_focus_changed: handle_forward_tabs, + restore_tab: handle_forward_tabs, + } + const { io, _ } = net(id) + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + const docs = DOCS(__filename)(opts.sid) + const actions_file = await drive.get('actions/command.json') + const actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + const focus_hat_action = { + name: 'Focus Wizard Hat', + info: 'Focus the wizard hat.', + icon: 'hat', + status: { hidden: true }, + steps: [], + run: focus_wizard_hat + } + docs.register_actions(actions.concat(focus_hat_action)) + on_hat_click.info = focus_hat_action.info + + io.on = { + up: io_up(), + tabs: io_tabs(), + task_manager: io_task_manager() + } + if (invite) { + io.accept(invite) + const data = { + type: 'wizard_hat', + sid: opts.sid + } + _.up('ui_focus', {}, data) + } + + shadow.innerHTML = ` +
+ + + + +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const hat_btn = shadow.querySelector('.hat-btn') + const bar_btn = shadow.querySelector('.bar-btn') + + const subs = await sdb.watch(onbatch) + + function onload (svg) { + const parser = new DOMParser() + const doc = parser.parseFromString(svg, 'image/svg+xml') + const svgElem = doc.documentElement + hat_btn.replaceChildren(svgElem) + hat_btn.onclick = docs.wrap_isolated(on_hat_click) + } + if (dricons[0]) { + onload(dricons[0]) + } + if (dricons[2]) { + const parser = new DOMParser() + const doc = parser.parseFromString(dricons[2], 'image/svg+xml') + const svgElem = doc.documentElement + bar_btn.replaceChildren(svgElem) + bar_btn.onclick = on_bar_btn_click + + function on_bar_btn_click () { + docs_toggle_active = !docs_toggle_active + // Send message to root module to set docs mode + _.up('set_docs_mode', {}, { active: docs_toggle_active }) + // Also send docs_toggle notification for UI updates + _.up('docs_toggle', {}, { active: docs_toggle_active }) + bar_btn.classList.toggle('active', docs_toggle_active) + _.task_manager('docs_toggle', {}, { active: docs_toggle_active }) + } + } + const tabs = await tabs_component({ ...subs[0] }, io.invite('tabs', { up: id })) + tabs.classList.add('tabs-bar') + shadow.querySelector('tabs').replaceWith(tabs) + + const task_mgr = await task_manager({ ...subs[1] }, io.invite('task_manager', { up: id })) + task_mgr.classList.add('bar-btn') + shadow.querySelector('task-manager').replaceWith(task_mgr) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function on_hat_click (event, $) { $('Focus Wizard Hat') } + function focus_wizard_hat () { _.up('ui_focus', {}, { type: 'wizard_hat', sid: opts.sid }) } + + function handle_docs_toggle (msg) { _.tabs(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_forward_tabs (msg) { _.tabs(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + + function onmessage_fail () { + // Handle other message types + } + + function io_tabs () { + return function tabs_protocol (msg) { + const action_handlers = { + update_tab_count: tabs_update_tab_count + } + const handler = action_handlers[msg.type] || tabs_forward_up + handler(msg) + + function tabs_update_tab_count (msg) { _.task_manager(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function tabs_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_task_manager () { + return function task_manager_protocol (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function inject_icons (data) { dricons = data } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + tabs: { + $: '' + }, + task_manager: { + $: '' + }, + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + tabs: { + 0: '', + mapping: { + icons: 'icons', + variables: 'variables', + scroll: 'scroll', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + task_manager: { + 0: '', + mapping: { + count: 'count', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .tabs-bar-container { + display: flex; + flex: inherit; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + } + .tabs-bar { + display: flex; + flex: auto; + flex-direction: row; + flex-wrap: nowrap; + align-items: stretch; + width: 256px; + } + .hat-btn, .bar-btn { + display: flex; + min-width: 32px; + border: none; + background: #131315; + cursor: pointer; + flex-direction: row; + justify-content: center; + align-items: center; + } + .bar-btn.active { + background: #2d4a6d; + } + ` + } + }, + 'icons/': { + '1.svg': { + $ref: 'hat.svg' + }, + '2.svg': { + $ref: 'hat.svg' + }, + '3.svg': { + $ref: 'docs.svg' + } + }, + 'actions/': { + 'command.json': { + raw: JSON.stringify([ + { + name: 'New File', + info: 'Create a new file after choosing its name and location.', + icon: 'file', + status: { + pinned: true, + default: true + }, + steps: [ + { + name: 'Enter File Name', + type: 'mandatory', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'search' } }, + { type: 'set_search_query', data: { query: 'file' } } + ] + }, + { name: 'Choose Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Open File', + info: 'Open an existing file from the selected location.', + icon: 'folder', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Select File', + type: 'mandatory', + is_completed: false, + component: 'form_input', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'default' } }, + { type: 'clear_selection', data: {} } + ] + } + ] + }, + { + name: 'Save File', + info: 'Save the current file to the chosen location and filename.', + icon: 'save', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Choose Location', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Enter File Name', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Settings', + info: 'Open configuration controls for the current workspace.', + icon: 'gear', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Configure Settings', + type: 'optional', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_flag', data: { flag_type: 'hubs', value: 'true' } } + ] + } + ] + }, + { + name: 'Help', + info: 'Open documentation for the current workspace.', + icon: 'help', + status: { + pinned: false, + default: false + }, + steps: [ + { name: 'View Documentation', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Terminal', + info: 'Open a terminal for the current workspace.', + icon: 'terminal', + status: { + pinned: true, + default: true + }, + steps: [ + { name: 'Open Terminal', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Search', + info: 'Search actions or workspace content using the command UI.', + icon: 'search', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Enter Search Query', + type: 'mandatory', + is_completed: false, + component: 'form_input', + status: 'default', + data: '', + commands: [ + { type: 'set_mode', data: { mode: 'search' } }, + { type: 'set_search_query', data: { query: 'action' } } + ] + }, + { + name: 'Select Scope', + type: 'optional', + is_completed: false, + component: 'input_test', + status: 'default', + data: '', + commands: [ + { type: 'set_flag', data: { flag_type: 'selection', value: 'default' } }, + { type: 'get_selected', data: {} } + ] + } + ] + }, + { + name: 'Click Rate Test', + info: 'Start a 10-click gesture. Regular clicks are documented as events; the 10th click triggers the result action.', + icon: 'timer', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Click 10 Times', + type: 'mandatory', + is_completed: false, + component: 'form_click_rate_test', + status: 'default', + data: '' + } + ] + }, + { + name: 'Split Tile', + info: 'Split the active tile in the selected direction.', + icon: 'split', + status: { + pinned: false, + default: true + }, + steps: [ + { + name: 'Choose Split Direction', + type: 'mandatory', + is_completed: false, + component: 'form_tile_split_choice', + status: 'default', + data: '' + } + ] + } + ]) + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/task_manager/README.md b/src/node_modules/task_manager/README.md new file mode 100644 index 0000000..afd14f2 --- /dev/null +++ b/src/node_modules/task_manager/README.md @@ -0,0 +1 @@ +A button component that displays the count of running tasks and emits ui_focus events when clicked. diff --git a/src/node_modules/task_manager/package.json b/src/node_modules/task_manager/package.json new file mode 100644 index 0000000..a4ec59a --- /dev/null +++ b/src/node_modules/task_manager/package.json @@ -0,0 +1 @@ +{"main": "task_manager.js"} diff --git a/src/node_modules/task_manager/task_manager.js b/src/node_modules/task_manager/task_manager.js new file mode 100644 index 0000000..8333597 --- /dev/null +++ b/src/node_modules/task_manager/task_manager.js @@ -0,0 +1,190 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const DOCS = require('DOCS') +const net = require('net_helper') + +module.exports = task_manager + +async function task_manager (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const docs = DOCS(__filename)(opts.sid) + const actions_file = await drive.get('actions/commands.json') + const actions = actions_file.raw + ? (typeof actions_file.raw === 'string' ? JSON.parse(actions_file.raw) : actions_file.raw) + : [] + + const on_message = { + update_tab_count: handle_update_count + } + + const on = { + style: inject, + count: update_count + } + const { io, _ } = net(id) + const focus_action = { + name: 'Focus Task Manager', + info: 'Focus the task manager.', + icon: 'tasks', + status: { hidden: true }, + steps: [], + run: focus_task_manager + } + docs.register_actions(actions.concat(focus_action)) + on_task_manager_click.info = focus_action.info + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+ +
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const btn = shadow.querySelector('.task-count-btn') + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + btn.onclick = docs.wrap_isolated(on_task_manager_click) + + await sdb.watch(onbatch) + + return el + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_fail + handler(msg) + } + } + + function on_task_manager_click (event, $) { $('Focus Task Manager') } + function focus_task_manager () { _.up('ui_focus', {}, { type: 'task_manager', sid: opts.sid }) } + + function handle_update_count (msg) { update_count(msg.data.count) } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn('invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function update_count (data) { if (btn) btn.textContent = data.toString() } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + DOCS: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + DOCS: { + 0: '' + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .task-count-btn { + background: #2d2d2d; + color: #fff; + border: none; + border-radius: 100%; + padding: 4px 8px; + min-width: 24px; + cursor: pointer; + display: flex; + align-items: center; + } + .task-count-btn:hover { + background: #3d3d3d; + } + ` + } + }, + 'actions/': { + 'commands.json': { + raw: JSON.stringify([ + { + name: 'Kill Process', + info: 'Stop the selected running process after confirmation.', + icon: 'stop', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Process', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Kill', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Restart Task', + info: 'Restart the selected task after confirmation.', + icon: 'refresh', + status: { + pinned: true, + default: false + }, + steps: [ + { name: 'Select Task', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'Confirm Restart', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + }, + { + name: 'Task Details', + info: 'Open details for the selected task.', + icon: 'info', + status: { + pinned: false, + default: true + }, + steps: [ + { name: 'Select Task', type: 'mandatory', is_completed: false, component: 'form_input', status: 'default', data: '' }, + { name: 'View Details', type: 'optional', is_completed: false, component: 'form_input', status: 'default', data: '' } + ] + } + ]) + } + }, + 'count/': { + 'value.json': { + raw: '3' + } + }, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/taskbar/README.md b/src/node_modules/taskbar/README.md new file mode 100644 index 0000000..27e84ad --- /dev/null +++ b/src/node_modules/taskbar/README.md @@ -0,0 +1 @@ +A bottom bar component containing the tabsbar and action_bar for providing unified navigation and action controls. diff --git a/src/node_modules/taskbar/package.json b/src/node_modules/taskbar/package.json new file mode 100644 index 0000000..6b2396e --- /dev/null +++ b/src/node_modules/taskbar/package.json @@ -0,0 +1,3 @@ +{ + "main": "taskbar.js" +} \ No newline at end of file diff --git a/src/node_modules/taskbar/taskbar.js b/src/node_modules/taskbar/taskbar.js new file mode 100644 index 0000000..4ecb2c1 --- /dev/null +++ b/src/node_modules/taskbar/taskbar.js @@ -0,0 +1,287 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') +const action_bar = require('action_bar') +const action_executor = require('action_executor') +const tabsbar = require('tabsbar') + +module.exports = taskbar + +async function taskbar (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + const on_message = { + update_steps_wizard_for_app: handle_update_steps_wizard_for_app, + docs_toggle: handle_docs_toggle, + load_actions: handle_load_actions, + step_clicked: handle_step_clicked, + update_quick_actions_for_app: handle_update_quick_actions_for_app, + update_quick_actions_input: handle_update_quick_actions_input, + show_submit_btn: handle_submit_btn_toggle, + hide_submit_btn: handle_submit_btn_toggle, + add_link_tab: handle_forward_tabsbar, + remove_link_tab: handle_forward_tabsbar, + show_collapsed_tab_group: handle_forward_tabsbar, + hide_collapsed_tab_group: handle_forward_tabsbar, + tile_focus_changed: handle_forward_tabsbar, + restore_tab: handle_forward_tabsbar + } + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + + shadow.innerHTML = ` +
+
+
+
+
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const action_executor_slot = shadow.querySelector('.action-executor-slot') + const action_bar_slot = shadow.querySelector('.action-bar-slot') + const tabsbar_slot = shadow.querySelector('.tabsbar-slot') + + const subs = await sdb.watch(onbatch) + io.on = { + up: io_up(), + action_bar: io_action_bar(), + action_executor: io_action_executor(), + tabsbar: io_tabsbar() + } + if (invite) io.accept(invite) + + const action_bar_el = await action_bar({ ...subs[0] }, io.invite('action_bar', { up: id })) + action_bar_el.classList.add('replaced-action-bar') + action_bar_slot.replaceWith(action_bar_el) + + const action_executor_el = await action_executor({ ...subs[1] }, io.invite('action_executor', { up: id })) + action_executor_el.classList.add('replaced-action-executor') + action_executor_slot.replaceWith(action_executor_el) + + const tabsbar_el = await tabsbar({ ...subs[2] }, io.invite('tabsbar', { up: id })) + tabsbar_el.classList.add('replaced-tabsbar') + tabsbar_slot.replaceWith(tabsbar_el) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + // --------- + // PROTOCOLS + // --------- + + function io_action_bar () { + return function action_bar_protocol (msg) { + const action_handlers = { + action_submitted: action_bar_forward_action_executor, + selected_action: action_bar_forward_action_executor, + activate_steps_wizard: action_bar_forward_action_executor, + render_form: action_bar_forward_action_executor, + console_history_toggle: action_bar_forward_up, + ui_focus: action_bar_forward_up, + display_actions: action_bar_forward_up, + filter_actions: action_bar_forward_up + } + const handler = action_handlers[msg.type] || action_bar_forward_up + handler(msg) + + function action_bar_forward_action_executor (msg) { _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function action_bar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_action_executor () { + return function action_executor_protocol (msg) { + console.error('taskbar: action_executor_protocol', msg.type, msg.data) + const action_handlers = { + load_actions: action_executor__forward_action_bar, + step_clicked: action_executor__forward_action_bar, + show_submit_btn: action_executor__forward_action_bar, + hide_submit_btn: action_executor__forward_action_bar, + action_auto_completed: action_executor__auto_completed + } + const handler = action_handlers[msg.type] || action_executor__noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function action_executor__forward_action_bar (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function action_executor__noop () {} + + function action_executor__auto_completed (msg) { _.action_bar('action_submitted', msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_tabsbar () { + return function tabsbar_protocol (msg) { + const action_handlers = { + docs_toggle: tabsbar_docs_toggle, + link_tab_close_clicked: tabsbar_forward_up, + link_tab_clicked: tabsbar_forward_up, + tab_group_tile_clicked: tabsbar_forward_up + } + const handler = action_handlers[msg.type] || tabsbar__noop + handler(msg) + _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + + function tabsbar_docs_toggle (msg) { + _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + } + + function tabsbar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function tabsbar__noop () {} + } + } + + function io_up () { + return function onmessage (msg) { + const handler = on_message[msg.type] || onmessage_forward_action_bar + handler(msg) + } + } + + function handle_update_steps_wizard_for_app (msg) { _.action_executor(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_docs_toggle (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_load_actions (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_step_clicked (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_update_quick_actions_for_app (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_update_quick_actions_input (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_submit_btn_toggle (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function handle_forward_tabsbar (msg) { _.tabsbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function onmessage_forward_action_bar (msg) { _.action_bar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + action_bar: { + $: '' + }, + action_executor: { + $: '' + }, + tabsbar: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance () { + return { + _: { + action_bar: { + 0: '', + mapping: { + icons: 'icons', + style: 'style', + variables: 'variables', + data: 'data', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + }, + action_executor: { + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs', + data: 'data' + } + }, + tabsbar: { + 0: '', + mapping: { + icons: 'icons', + style: 'style', + docs: 'docs', + actions: 'actions' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .taskbar-container { + display: flex; + background: #2d2d2d; + column-gap: 1px; + flex-direction: column; + align-content: center; + justify-content: center; + container-type: inline-size; + } + .replaced-tabsbar { + display: flex; + flex: auto; + } + .replaced-action-bar { + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; + background: #131315; + } + .replaced-action-executor { + display: flex; + } + .bottom-slot { + display: flex; + flex-direction: row; + justify-content: space-between; + } + @container (max-width: 768px) { + .bottom-slot { + flex-direction: column; + } + } + ` + } + }, + 'icons/': {}, + 'variables/': {}, + 'data/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'prefs/': {}, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/theme_widget/README.md b/src/node_modules/theme_widget/README.md new file mode 100644 index 0000000..af189a5 --- /dev/null +++ b/src/node_modules/theme_widget/README.md @@ -0,0 +1 @@ +The root application component that composes space, taskbar, focus_tracker, and control_unit with protocol-based message routing. diff --git a/src/node_modules/theme_widget/package.json b/src/node_modules/theme_widget/package.json new file mode 100644 index 0000000..e0b0d9c --- /dev/null +++ b/src/node_modules/theme_widget/package.json @@ -0,0 +1,3 @@ +{ + "main": "theme_widget.js" +} \ No newline at end of file diff --git a/src/node_modules/theme_widget/theme_widget.js b/src/node_modules/theme_widget/theme_widget.js new file mode 100644 index 0000000..e899948 --- /dev/null +++ b/src/node_modules/theme_widget/theme_widget.js @@ -0,0 +1,277 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const program_container = require('program_container') +const taskbar = require('taskbar') + +module.exports = theme_widget + +async function theme_widget (opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject, + focused: handle_focused + } + + // Inline focus tracking (merged from focus_tracker) + let last_focused = null + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` +
+
+
+
+ ` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const program_container_slot = shadow.querySelector('.program-container-slot') + const taskbar_slot = shadow.querySelector('.taskbar-slot') + + const subs = await sdb.watch(onbatch) + + let program_container_el = null + let taskbar_el = null + io.on = { + up: io_up(), + program_container: io_program_container(), + taskbar: io_taskbar() + } + if (invite) io.accept(invite) + + taskbar_el = await taskbar({ ...subs[1] }, io.invite('taskbar', { up: id })) + taskbar_el.classList.add('taskbar') + taskbar_slot.replaceWith(taskbar_el) + + program_container_el = await program_container({ ...subs[0] }, io.invite('program_container', { up: id })) + program_container_el.classList.add('program-container') + program_container_slot.replaceWith(program_container_el) + + return el + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + + function inject ({ data }) { sheet.replaceSync(data[0]) } + + // Inline focus tracker: reads persisted focused value to keep last_focused in sync + function handle_focused ({ data }) { + const focused = typeof data[0] === 'string' ? JSON.parse(data[0]) : data[0] + last_focused = focused.value + } + + function fail ({ data, type }) { console.warn('invalid message', { cause: { data, type } }) } + + function io_up () { + return function onmessage_from_root (msg) { + const action_handlers = { + update_actions_for_app: root_update_actions_for_app, + update_quick_actions_for_app: root_forward_taskbar, + update_steps_wizard_for_app: root_forward_taskbar, + tile_focus_changed: root_forward_taskbar, + show_collapsed_tab_group: root_forward_taskbar, + hide_collapsed_tab_group: root_forward_taskbar + } + const handler = action_handlers[msg.type] || fail + handler(msg) + + function root_update_actions_for_app (msg) { + if (_.program_container) _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) + else setTimeout(root_retry_send_program_container, 500, msg) + } + + function root_retry_send_program_container (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function root_forward_taskbar (msg) { _.taskbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + // Inline focus tracker: handles ui_focus messages from children + function handle_ui_focus (msg) { + if (last_focused !== msg.data.type) { + _.up('focused_app_changed', {}, msg.data) + } + drive.put('focused/current.json', { value: msg.data.type }) + } + + // --------- + // PROTOCOLS + // --------- + function io_program_container () { + return function program_container_protocol (msg) { + const action_handlers = { + ui_focus: program_container_forward_ui_focus, + set_doc_display_handler: program_container_forward_up, + action_auto_completed: program_container_forward_up, + action_complete: program_container_forward_up + } + const handler = action_handlers[msg.type] || program_container_forward_taskbar + handler(msg) + + function program_container_forward_ui_focus (msg) { handle_ui_focus(msg) } + function program_container_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function program_container_forward_taskbar (msg) { _.taskbar(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } + + function io_taskbar () { + return function taskbar_protocol (msg) { + const action_handlers = { + ui_focus: taskbar_forward_ui_focus, + docs_toggle: taskbar_docs_toggle, + set_docs_mode: taskbar_forward_up, + action_auto_completed: taskbar_forward_up, + action_complete: taskbar_forward_up, + link_tab_close_clicked: taskbar_forward_up, + link_tab_clicked: taskbar_forward_up, + tab_group_tile_clicked: taskbar_forward_up + } + const handler = action_handlers[msg.type] || taskbar_forward_program_container + handler(msg) + + function taskbar_forward_ui_focus (msg) { handle_ui_focus(msg) } + function taskbar_forward_up (msg) { _.up(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function taskbar_forward_program_container (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + function taskbar_docs_toggle (msg) { _.program_container(msg.type, msg.head ? { cause: msg.head } : {}, msg.data) } + } + } +} + +function fallback_module () { + return { + api: fallback_instance, + _: { + program_container: { + $: '' + }, + taskbar: { + $: '' + }, + net_helper: { + $: '' + } + }, + drive: {} + } + + function fallback_instance () { + return { + _: { + program_container: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs: 'docs', + docs_style: 'docs_style' + } + }, + taskbar: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + hardcons: 'hardcons', + docs: 'docs' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .theme-widget { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: #131315; + min-height: 0; + min-width: 400px; + } + .program-container { + flex: 1 1 auto; + min-height: 0; + height: 100%; + } + .taskbar { + flex: 0 0 auto; + width: 100%; + z-index: 10; + } + ` + } + }, + 'flags/': {}, + 'commands/': {}, + 'icons/': {}, + 'scroll/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'active_tab/': {}, + 'entries/': {}, + 'runtime/': {}, + 'mode/': {}, + 'keybinds/': {}, + 'undo/': {}, + 'focused/': { + 'current.json': { + raw: { value: 'default' } + } + }, + 'temp_actions/': {}, + 'temp_quick_actions/': {}, + 'prefs/': {}, + 'variables/': {}, + 'data/': {}, + 'docs_style/': {}, + 'docs/': { + 'README.md': { + $ref: 'README.md' + } + } + } + } + } +} diff --git a/src/node_modules/tile_manager/README.md b/src/node_modules/tile_manager/README.md new file mode 100644 index 0000000..6266cd0 --- /dev/null +++ b/src/node_modules/tile_manager/README.md @@ -0,0 +1,46 @@ +# tile_manager + +Manages multiple tiles in a tiled layout. Tile `0` is a `theme_widget` (it owns the shared +taskbar and tab strip), every tile created by a split is a `tab_group`. + +## Architecture + +``` +page.js (root) + ↓ +tile_manager ← handles layout tree, split actions, collapse levels + ↓ +theme_widget #0 tab_group #1 tab_group #2 ... +(full VM) (tabs + editor) (tabs + editor) +``` + +## Supported Directions + +- `right` — new tile appears to the right +- `left` — new tile appears to the left +- `up` — new tile appears above +- `down` — new tile appears below + +## Collapse levels + +The layout is a tree of `split` nodes and `leaf` tiles. `max_level` is the depth of that +tree, and `collapse_level` says how many split levels are currently folded away, counted +from the deepest splits upwards: + +| collapse_level | rendered | folded away | +| -------------- | ------------------- | ------------------- | +| `0` | the whole tree | nothing | +| `1 … max - 1` | the outer splits | the deepest tiles | +| `max` | one tile | every other tile | + +A folded group is reported to whichever tile ends up rendered in its place, not always +to tile `0` — split root `down` then split that tile `right`, shrink, and the folded tab +shows up in the down tile's own strip, since tile `0` was never part of that split. + +The level is picked from the observed size of the component: `MIN_TILE_WIDTH` / +`MIN_TILE_HEIGHT` per tile, added up along each split direction. Clicking a folded tab +overrides the level just enough to show that tile again; the next resize hands control +back to the responsive level. + +See [guide/tile-manager-collapse-levels.md](../../../guide/tile-manager-collapse-levels.md) +for the design notes and the browser check. diff --git a/src/node_modules/tile_manager/package.json b/src/node_modules/tile_manager/package.json new file mode 100644 index 0000000..388fe99 --- /dev/null +++ b/src/node_modules/tile_manager/package.json @@ -0,0 +1,4 @@ +{ + "name": "tile_manager", + "main": "tile_manager.js" +} diff --git a/src/node_modules/tile_manager/style/tile_manager.css b/src/node_modules/tile_manager/style/tile_manager.css new file mode 100644 index 0000000..efce33a --- /dev/null +++ b/src/node_modules/tile_manager/style/tile_manager.css @@ -0,0 +1,65 @@ +.tile-manager-wrapper { + --highlight-color: rgba(103, 195, 255, 0.85); + --highlight-color-dim: rgba(103, 195, 255, 0.35); + display: flex; + flex-direction: column; + width: 100%; + height: 100%; + background: #131315; + min-height: 0; +} + +.tile-manager { + display: flex; + flex: 1; + width: 100%; + min-height: 0; +} + +.tile-manager > *, +.tile-split > * { + flex: 1; + min-height: 0; + min-width: 0; +} + +.tile-split { + display: flex; + flex: 1; + min-height: 0; + min-width: 0; + transition: flex 0.3s ease, opacity 0.25s ease; +} + +.tile-manager.horizontal, +.tile-split.horizontal { + flex-direction: row; +} + +.tile-manager.vertical, +.tile-split.vertical { + flex-direction: column; +} + +.tile-slot { + flex: 1; + min-height: 0; + min-width: 0; + position: relative; + overflow: hidden; + border: 2px solid rgba(255, 255, 255, 0.08); + transition: border-color 0.2s ease, opacity 0.25s ease, flex 0.3s ease; +} + +.tile-slot.focused { + border-color: var(--highlight-color); + box-shadow: 0 0 8px rgba(103, 195, 255, 0.2); +} + +.tile-slot > * { + width: 100%; + height: 100%; +} + +/* Collapse levels are applied by the render pipeline: folded tiles are removed from + the DOM instead of hidden with CSS, so every level renders its own real layout. */ diff --git a/src/node_modules/tile_manager/tile_manager.js b/src/node_modules/tile_manager/tile_manager.js new file mode 100644 index 0000000..3e012e7 --- /dev/null +++ b/src/node_modules/tile_manager/tile_manager.js @@ -0,0 +1,699 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const { get } = statedb(fallback_module) +const net = require('net_helper') + +const theme_widget = require('theme_widget') +const tab_group = require('tab_group') + +module.exports = tile_manager + +async function tile_manager(opts, invite) { + const { id, sdb } = await get(opts.sid) + const { drive } = sdb + + const on = { + style: inject + } + + const layout = { + root: { type: 'leaf', tile_id: 0, el: null }, + collapse_level: 0, + manual_level: null, + focused_tile: 0 + } + const tile_registry = {} + + const cached_actions = { + update_actions_for_app: null, + update_quick_actions_for_app: null, + update_steps_wizard_for_app: null + } + + const tile_tabs = {} + + let tile_counter = 1 + let split_counter = 1 + let last_width = 0 + let last_height = 0 + const MIN_TILE_WIDTH = 300 + const MIN_TILE_HEIGHT = 200 + const { io, _ } = net(id) + + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = `
+
+
` + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + const wrapper = shadow.querySelector('.tile-manager-wrapper') + const container = shadow.querySelector('.tile-manager') + + const subs = await sdb.watch(onbatch) + + io.on = { + up: io_up() + } + if (invite) io.accept(invite) + + const resize_observer = new ResizeObserver(on_resize_observed) + resize_observer.observe(el) + + container.addEventListener('pointerdown', function (e) { + const slot = e.target.closest ? e.target.closest('.tile-slot') : null + if (!slot) return + const tile_id = parseInt(slot.getAttribute('data-tile-id'), 10) + if (!isNaN(tile_id) && tile_id !== layout.focused_tile) { + set_focused_tile(tile_id) + } + }, true) + + function on_resize_observed(entries) { + for (const entry of entries) { + handle_resize(entry.contentRect.width, entry.contentRect.height) + } + } + + await render_layout_tree() + + return el + + function get_tree_depth(node) { + if (!node) return 0 + if (node.type === 'leaf') return 0 + let max_child_depth = 0 + for (const child of node.children) { + const d = get_tree_depth(child) + if (d > max_child_depth) max_child_depth = d + } + return 1 + max_child_depth + } + + function get_max_collapse_level() { + return get_tree_depth(layout.root) + } + + function pick_representative_child(node) { + return node.children.find(has_tile_0) || node.children[0] + } + + function has_tile_0(node) { + if (!node) return false + if (node.type === 'leaf') return node.tile_id === 0 + return node.children.some(child => has_tile_0(child)) + } + + function resolve_representative_leaf(node) { + if (node.type === 'leaf') return node.tile_id + return resolve_representative_leaf(pick_representative_child(node)) + } + + function measure_min_size(node, depth, prune_depth) { + if (!node) return { width: 0, height: 0 } + if (node.type === 'leaf') return { width: MIN_TILE_WIDTH, height: MIN_TILE_HEIGHT } + if (depth >= prune_depth) return measure_min_size(pick_representative_child(node), depth + 1, prune_depth) + + const sizes = node.children.map(measure_child) + const widths = sizes.map(size => size.width) + const heights = sizes.map(size => size.height) + if (node.direction === 'horizontal') return { width: sum(widths), height: Math.max(...heights) } + return { width: Math.max(...widths), height: sum(heights) } + + function measure_child(child) { return measure_min_size(child, depth + 1, prune_depth) } + } + + function sum(values) { return values.reduce(add_value, 0) } + + function add_value(total, value) { return total + value } + + function pick_collapse_level(width, height) { + const max_level = get_max_collapse_level() + for (let level = 0; level < max_level; level++) { + const size = measure_min_size(layout.root, 0, max_level - level) + if (size.width <= width && size.height <= height) return level + } + return max_level + } + + function pick_level_showing_tile(tile_id) { + const max_level = get_max_collapse_level() + for (let level = max_level; level > 0; level--) { + if (collect_leaf_ids(layout.root, max_level - level).includes(tile_id)) return level + } + return 0 + } + + function handle_resize(width, height) { + if (!width || !height) return + if (width === last_width && height === last_height) return + last_width = width + last_height = height + layout.manual_level = null + refresh_layout().catch(on_layout_error) + } + + function set_collapse_level(level) { + const max_level = get_max_collapse_level() + layout.manual_level = Math.max(0, Math.min(level, max_level)) + console.error('tile_manager: set_collapse_level', layout.manual_level, '(max:', max_level, ')') + refresh_layout().catch(on_layout_error) + } + + function refresh_layout() { + const responsive_level = last_width ? pick_collapse_level(last_width, last_height) : layout.collapse_level + layout.collapse_level = layout.manual_level === null ? responsive_level : layout.manual_level + return render_layout_tree() + } + + function on_layout_error(err) { console.error('tile_manager: layout render failed', err) } + + function get_tiles() { + return collect_leaf_ids().map(tile_id => tile_registry[tile_id]).filter(Boolean) + } + + function collect_leaf_ids(node = layout.root, prune_depth = Infinity, depth = 0, tile_ids = []) { + if (!node) return tile_ids + if (node.type === 'leaf') { + tile_ids.push(node.tile_id) + return tile_ids + } + if (depth >= prune_depth) { + return collect_leaf_ids(pick_representative_child(node), prune_depth, depth + 1, tile_ids) + } + node.children.forEach(child => collect_leaf_ids(child, prune_depth, depth + 1, tile_ids)) + return tile_ids + } + + function group_hidden_groups_by_anchor(node, depth, prune_depth, groups_by_tile = new Map()) { + if (!node || node.type === 'leaf') return groups_by_tile + if (depth < prune_depth) { + node.children.forEach(child => group_hidden_groups_by_anchor(child, depth + 1, prune_depth, groups_by_tile)) + return groups_by_tile + } + + const kept_child = pick_representative_child(node) + const hidden_children = node.children.filter(child => child !== kept_child) + if (hidden_children.length) { + const anchor_tile_id = resolve_representative_leaf(kept_child) + const group = { + type: 'collapsed_group', + split_id: node.split_id, + direction: node.direction, + depth: depth, + children: hidden_children.map(describe_subtree) + } + const groups = groups_by_tile.get(anchor_tile_id) || [] + groups.push(group) + groups_by_tile.set(anchor_tile_id, groups) + } + return group_hidden_groups_by_anchor(kept_child, depth + 1, prune_depth, groups_by_tile) + } + + function describe_subtree(node) { + if (!node) return null + if (node.type === 'leaf') { + return { + type: 'leaf', + tile_id: node.tile_id, + tabs: tile_tabs[node.tile_id] || [], + children: [] + } + } + return { + type: 'split_group', + split_id: node.split_id, + direction: node.direction, + children: node.children.map(describe_subtree).filter(Boolean) + } + } + + function find_leaf_node(tile_id, node = layout.root, parent = null, index = -1) { + if (!node) return null + if (node.type === 'leaf') { + return node.tile_id === tile_id ? { node, parent, index } : null + } + for (let child_index = 0; child_index < node.children.length; child_index++) { + const match = find_leaf_node(tile_id, node.children[child_index], node, child_index) + if (match) return match + } + return null + } + + function replace_leaf_node(tile_id, next_node) { + const match = find_leaf_node(tile_id) + if (!match) return false + if (!match.parent) { + layout.root = next_node + return true + } + match.parent.children[match.index] = next_node + return true + } + + async function ensure_tile(tile_id) { + if (tile_registry[tile_id]) return tile_id + + const sub_entry = subs[tile_id] || { sid: opts.sid } + const tile_info = { + id: tile_id, + element: null, + slot: null, + sid: sub_entry.sid + } + tile_registry[tile_id] = tile_info + console.error('tile_manager: tile_info added for tile', tile_id) + + io.on[`tile_${tile_id}`] = io_tile(tile_id) + console.error('tile_manager: handler registered for tile_' + tile_id) + const tile_invite = io.invite(`tile_${tile_id}`, { up: id }) + console.error('tile_manager: invite created for tile_' + tile_id, '_[tile_' + tile_id + '] exists:', !!_[`tile_${tile_id}`]) + + const component = tile_id === 0 ? theme_widget : tab_group + const tile_el = await component( + { ...sub_entry, ids: { up: id } }, + tile_invite + ) + + tile_info.element = tile_el + + console.error('tile_manager: created tile', tile_id) + return tile_id + } + + async function render_layout_tree() { + const max_level = get_max_collapse_level() + const collapse_level = Math.max(0, Math.min(layout.collapse_level, max_level)) + const prune_depth = max_level - collapse_level + + layout.collapse_level = collapse_level + wrapper.setAttribute('data-collapse-level', collapse_level) + wrapper.setAttribute('data-max-level', max_level) + + const rendered_tree = await render_node(layout.root, 0, prune_depth) + sync_dom_children(container, rendered_tree ? [rendered_tree] : []) + + sync_collapsed_tab_strip(collapse_level, max_level, prune_depth) + set_focused_tile(pick_visible_focus(prune_depth)) + } + + async function render_node(node, depth, prune_depth) { + if (!node) return null + if (node.type === 'leaf') return render_leaf_node(node.tile_id) + if (depth >= prune_depth) return render_node(pick_representative_child(node), depth + 1, prune_depth) + + if (!node.el) node.el = document.createElement('div') + node.el.className = `tile-split ${node.direction}` + + const child_elements = [] + for (const child of node.children) { + const child_el = await render_node(child, depth + 1, prune_depth) + if (child_el) child_elements.push(child_el) + } + sync_dom_children(node.el, child_elements) + return node.el + } + + function sync_collapsed_tab_strip(collapse_level, max_level, prune_depth) { + const groups_by_tile = collapse_level > 0 ? group_hidden_groups_by_anchor(layout.root, 0, prune_depth) : new Map() + + for (const tile_id of collect_leaf_ids(layout.root, prune_depth)) { + const send = _[`tile_${tile_id}`] + if (!send) continue + + const collapsed_groups = groups_by_tile.get(tile_id) + if (!collapsed_groups) { + send('hide_collapsed_tab_group', {}, {}) + continue + } + + send('show_collapsed_tab_group', {}, { + nested: true, + collapse_level: collapse_level, + max_level: max_level, + collapsed_groups: collapsed_groups + }) + } + } + + function pick_visible_focus(prune_depth) { + const visible_ids = collect_leaf_ids(layout.root, prune_depth) + if (visible_ids.includes(layout.focused_tile)) return layout.focused_tile + return visible_ids.length ? visible_ids[0] : layout.focused_tile + } + + async function render_leaf_node(tile_id) { + const match = find_leaf_node(tile_id) + const node = match?.node + if (!node) return null + await ensure_tile(tile_id) + const tile_info = tile_registry[tile_id] + if (!tile_info) return null + + if (!node.el) { + node.el = document.createElement('div') + node.el.className = 'tile-slot' + } + node.el.setAttribute('data-tile-id', tile_id) + tile_info.slot = node.el + + if (node.el.firstChild !== tile_info.element) { + node.el.replaceChildren(tile_info.element) + } + return node.el + } + + function sync_dom_children(parent, desired_children) { + let current_index = 0 + + for (const child of desired_children) { + const current_child = parent.childNodes[current_index] + if (current_child !== child) { + parent.insertBefore(child, current_child || null) + } + current_index++ + } + + while (parent.childNodes.length > desired_children.length) { + parent.removeChild(parent.lastChild) + } + } + + function io_tile(tile_id) { + return function tile_protocol(msg) { + const { type, data } = msg + console.error(`tile_manager: message from tile_${tile_id}`, type, data) + + if (type === 'tab_group_tile_clicked') { + const target_tile = data && typeof data.tile_id === 'number' ? data.tile_id : null + console.error('tile_manager: folded tab clicked, revealing tile', target_tile) + if (target_tile !== null) layout.focused_tile = target_tile + set_collapse_level(target_tile === null ? 0 : pick_level_showing_tile(target_tile)) + return + } + + if (tile_id === 0 && type === 'request_collapse_level') { + if (data && typeof data.level === 'number') { + set_collapse_level(data.level) + } + return + } + + if (type === 'ui_focus') { + set_focused_tile(tile_id) + } + + if (type === 'tab_name_clicked' && tile_id !== 0) { + if (data && data.id && tile_tabs[tile_id]) { + const exists = tile_tabs[tile_id].find(t => t.id === data.id) + if (!exists) { + tile_tabs[tile_id].push({ id: data.id, name: data.name, program: data.program }) + } + } + } + if (type === 'tab_close_clicked' && tile_id !== 0) { + if (data && data.id && tile_tabs[tile_id]) { + tile_tabs[tile_id] = tile_tabs[tile_id].filter(t => t.id !== data.id) + } + } + + if (tile_id !== 0 && type === 'all_tabs_closed') { + console.error('tile_manager: all tabs closed in split tile', tile_id) + handle_merge(tile_id) + return + } + + if (type === 'action_auto_completed' || type === 'action_complete') { + console.error('tile_manager: action completed, checking for split', data) + const action = data?.selected_action + console.error('tile_manager: action name:', action?.name) + if (action?.name === 'Split Tile') { + let direction = null + + if (data.result) { + try { + const results = JSON.parse(data.result) + direction = results[0] + console.error('tile_manager: direction from result:', direction) + } catch (e) { + console.error('tile_manager: failed to parse result', e) + } + } + + if (!direction) { + const split_step = action.steps.find(s => s.component === 'form_tile_split_choice') + console.error('tile_manager: split_step found:', split_step) + if (split_step && split_step.data) { + direction = split_step.data + } + } + + if (direction) { + console.error('tile_manager: split requested', direction, 'from tile', tile_id) + handle_split(tile_id, direction) + } else { + console.error('tile_manager: no direction found, cannot split') + } + } + } + + const refs = msg.head ? { cause: msg.head } : {} + _.up?.(type, refs, data) + } + } + + async function handle_split(source_tile_id, direction) { + const source_match = find_leaf_node(source_tile_id) + if (!source_match) { + console.error('tile_manager: source tile not found for split', source_tile_id) + return + } + + const new_tile_id = tile_counter++ + const new_leaf = { type: 'leaf', tile_id: new_tile_id, el: null } + const source_leaf = source_match.node + const is_horizontal = direction === 'left' || direction === 'right' + const split_node = { + type: 'split', + split_id: split_counter++, + direction: is_horizontal ? 'horizontal' : 'vertical', + split_direction: direction, + children: direction === 'left' || direction === 'up' + ? [new_leaf, source_leaf] + : [source_leaf, new_leaf], + el: null + } + + replace_leaf_node(source_tile_id, split_node) + + await ensure_tile(new_tile_id) + send_cached_actions_to_tile(new_tile_id) + + const new_tile_send = _[`tile_${new_tile_id}`] + if (new_tile_send) { + const tab_data = { name: 'New Tab', program: 'text_editor' } + new_tile_send('create_default_tab', {}, tab_data) + if (!tile_tabs[new_tile_id]) tile_tabs[new_tile_id] = [] + tile_tabs[new_tile_id].push({ id: `tab_initial_${new_tile_id}`, name: tab_data.name, program: tab_data.program }) + } + + await refresh_layout() + + console.error('tile_manager: split complete', direction, get_tiles().length, 'tiles') + } + + function handle_merge(tile_id_to_remove) { + console.error('tile_manager: handle_merge', tile_id_to_remove) + + const match = find_leaf_node(tile_id_to_remove) + if (!match || !match.parent) { + console.error('tile_manager: tile not found for merge', tile_id_to_remove) + return + } + + const sibling_index = match.index === 0 ? 1 : 0 + const sibling_node = match.parent.children[sibling_index] + + if (match.parent === layout.root) { + layout.root = sibling_node + } else { + const did_replace = replace_split_parent(layout.root, match.parent, sibling_node) + if (!did_replace) { + console.error('tile_manager: failed to replace merge parent for tile', tile_id_to_remove) + return + } + } + + const tile_info = tile_registry[tile_id_to_remove] + if (tile_info?.element?.parentNode) { + tile_info.element.parentNode.remove() + } + delete tile_registry[tile_id_to_remove] + delete tile_tabs[tile_id_to_remove] + + refresh_layout().catch(on_layout_error) + + console.error('tile_manager: merge complete, tiles remaining:', get_tiles().length) + } + + function replace_split_parent(node, target_parent, replacement_node) { + if (!node || node.type === 'leaf') return false + for (let index = 0; index < node.children.length; index++) { + if (node.children[index] === target_parent) { + node.children[index] = replacement_node + return true + } + if (replace_split_parent(node.children[index], target_parent, replacement_node)) return true + } + return false + } + + function set_focused_tile(tile_id) { + console.error('tile_manager: set_focused_tile', tile_id) + layout.focused_tile = tile_id + + for (const tile of get_tiles()) { + const slot = tile.slot + if (slot) { + slot.classList.toggle('focused', tile.id === tile_id) + } + } + + for (const tile of get_tiles()) { + const send = _[`tile_${tile.id}`] + if (send) { + send('tile_focus_changed', {}, { focused_tile: tile_id, is_focused: tile.id === tile_id }) + } + } + } + + function send_cached_actions_to_tile(tile_id) { + const send = _[`tile_${tile_id}`] + if (!send) { + console.error('tile_manager: cannot send cached actions, tile_' + tile_id + ' not found') + return + } + + console.error('tile_manager: sending cached actions to tile_' + tile_id) + + for (const [type, data] of Object.entries(cached_actions)) { + if (data !== null) { + console.error('tile_manager: sending cached', type, 'to tile_' + tile_id) + send(type, {}, data) + } + } + } + + function io_up() { + const cacheable = { + update_actions_for_app: true, + update_quick_actions_for_app: true, + update_steps_wizard_for_app: true + } + return function onmessage(msg) { + const { type, data } = msg + console.error('tile_manager: message from root', type, 'tiles:', get_tiles().length) + if (cacheable[type]) { + cached_actions[type] = data + console.error('tile_manager: cached', type) + } + const refs = msg.head ? { cause: msg.head } : {} + for (const tile of get_tiles()) { + const send = _[`tile_${tile.id}`] + if (send) send(type, refs, data) + } + } + } + + async function onbatch(batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func({ data, type }) + } + + function load_path_raw(path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw(file) { return file.raw } + } + + function inject({ data }) { sheet.replaceSync(data[0]) } + + function fail({ data, type }) { console.warn('tile_manager: invalid message', { cause: { data, type } }) } +} + +function fallback_module() { + return { + api: fallback_instance, + _: { + theme_widget: { + $: '' + }, + tab_group: { + $: '' + }, + net_helper: { + $: '' + } + } + } + + function fallback_instance() { + return { + _: { + theme_widget: { + 0: '', + mapping: { + style: 'style', + icons: 'icons', + commands: 'commands', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs: 'docs', + docs_style: 'docs_style' + } + }, + tab_group: { + 1: '', + 2: '', + 3: '', + 4: '', + 5: '', + 6: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + docs: 'docs', + docs_style: 'docs_style' + } + }, + net_helper: { + 0: '' + } + }, + drive: { + 'style/': { + 'tile_manager.css': { + $ref: 'style/tile_manager.css' + } + } + } + } + } +} diff --git a/src/node_modules/ui_gallery/index.js b/src/node_modules/ui_gallery/index.js new file mode 100644 index 0000000..e54a973 --- /dev/null +++ b/src/node_modules/ui_gallery/index.js @@ -0,0 +1,830 @@ +const STATE = require('STATE') +const statedb = STATE(__filename) +const admin_api = statedb.admin() +const admin_on = {} +admin_api.on(handle_admin_message) +const { sdb, io: sdbio, id } = statedb(fallback_module) +const { drive, admin } = sdb +const net = require('net_helper') +const DOCS = require('DOCS') +const docs = DOCS(__filename)() +const docs_admin = docs.admin +/****************************************************************************** + PAGE +******************************************************************************/ +const navbar = require('menu') +const theme_widget = require('theme_widget') +const taskbar = require('taskbar') +const tabsbar = require('tabsbar') +const action_bar = require('action_bar') +const program_container = require('program_container') +const tabs = require('tabs') +const console_history = require('console_history') +const tile_manager = require('tile_manager') +const actions = require('actions') +const tabbed_editor = require('tabbed_editor') +const task_manager = require('task_manager') +const quick_actions = require('quick_actions') +const graph_viewer = require('graph_viewer') +const editor = require('quick_editor') +const action_executor = require('action_executor') +const steps_wizard = require('steps_wizard') +const { resource } = require('helpers') + +const imports = { + tile_manager, + theme_widget, + taskbar, + tabsbar, + action_bar, + program_container, + tabs, + console_history, + actions, + tabbed_editor, + task_manager, + quick_actions, + graph_viewer, + action_executor, + steps_wizard +} +module.exports = ui_gallery + +/****************************************************************************** + PAGE BOOT +******************************************************************************/ +async function ui_gallery (opts = {}) { + // ---------------------------------------- + // ID + JSON STATE + // ---------------------------------------- + let resize_enabled = true + const on = { + style: inject, + resize_container: update_resize, + ...sdb.admin.status.dataset.drive, + ...sdb.admin + } + // const status = {} + // ---------------------------------------- + // TEMPLATE + // ---------------------------------------- + const el = document.createElement('div') + const shadow = el.attachShadow({ mode: 'closed' }) + shadow.innerHTML = ` + +
+
+
` + document.body.style.margin = 0 + document.body.style.backgroundColor = '#d8dee9' + + // ---------------------------------------- + // ELEMENTS + // ---------------------------------------- + + const navbar_slot = shadow.querySelector('.navbar-slot') + const components_wrapper = shadow.querySelector('.components-wrapper') + const sheet = new CSSStyleSheet() + shadow.adoptedStyleSheets = [sheet] + + const entries = Object.entries(imports) + const wrappers = [] + const names = entries.map(get_entry_name) + let current_selected_wrapper = null + + function get_entry_name (entry) { return entry[0] } + + const url_params = new URLSearchParams(window.location.search) + const checked_param = url_params.get('checked') + const selected_name_param = url_params.get('selected') + let initial_checked_indices = [] + + if (checked_param) { + try { + const parsed = JSON.parse(checked_param) + if (Array.isArray(parsed) && parsed.every(Number.isInteger)) { + initial_checked_indices = parsed + } else { + console.warn('Invalid "checked" URL parameter format.') + } + } catch (e) { + console.error('Error parsing "checked" URL parameter:', e) + } + } + + const menu_callbacks = { + on_checkbox_change: handle_checkbox_change, + on_label_click: handle_label_click, + on_select_all_toggle: handle_select_all_toggle, + on_resize_toggle: handle_resize_toggle + } + const item = resource() + sdbio.on(register_io_port) + const { io, _: send } = net(id) + io.on = { + theme_widget: io_theme_widget(), + up: io_up() + } + const preview_names = Object.keys(imports) + for (const name of preview_names) { + if (name === 'theme_widget') continue + io.on[name] = io_noop() + } + + function register_io_port (port) { + const { by, to } = port + item.set(port.to, port) + + port.onmessage = on_port_message + + function on_port_message (event) { + const txt = event.data + const key = `[${by} -> ${to}]` + console.log('[ port-stuff ]', key) + + on[txt.type](...txt.data) + } + } + + const editor_subs = await sdb.get_sub('ui_gallery>quick_editor') + // const subs = await sdb.watch(onbatch) + const subs = (await sdb.watch(onbatch)).filter(is_even_index) + + function is_even_index (_, index) { return index % 2 === 0 } + + console.log('Page subs', subs) + const nav_menu_element = await navbar(subs[names.length], names, initial_checked_indices, menu_callbacks) + + const main_editor = editor_subs[0] ? await editor(editor_subs[0]) : null + navbar_slot.replaceWith(nav_menu_element, main_editor || document.createElement('div')) + await create_component(entries) + update_resize(resize_enabled) + window.onload = scroll_to_initial_selected + send_quick_editor_data() + admin_on.import = send_quick_editor_data + + function io_theme_widget () { + return function theme_widget_protocol (msg) { + const action_handlers = { + set_docs_mode: handle_set_docs_mode, + set_doc_display_handler: handle_set_doc_display_handler, + focused_app_changed: handle_focused_app_changed + } + const handler = action_handlers[msg.type] || handle_fail + handler(msg) + + function handle_set_docs_mode (msg) { docs_admin.set_docs_mode(msg.data.active) } + function handle_set_doc_display_handler (msg) { docs_admin.set_doc_display_handler(msg.data.callback) } + function handle_fail (msg) { console.warn('page: unhandled message from theme_widget', msg) } + + function handle_focused_app_changed (msg) { + const actions = docs_admin.get_actions(msg.data.sid) + update_actions_for_app(actions, msg) + } + + async function update_actions_for_app (data, msg) { + const refs = msg.head ? { cause: msg.head } : {} + send.theme_widget('update_actions_for_app', refs, data) + send.theme_widget('update_quick_actions_for_app', refs, data) + send.theme_widget('update_steps_wizard_for_app', refs, data) + } + } + } + + function io_up () { + return function () {} + } + + function io_noop () { + return function () {} + } + return el + async function create_component (entries_obj) { + let index = 0 + const component_counters = {} + + for (const [name, factory] of entries_obj) { + const is_initially_checked = initial_checked_indices.length === 0 || initial_checked_indices.includes(index + 1) + const outer = document.createElement('div') + outer.className = 'component-outer-wrapper' + outer.style.display = is_initially_checked ? 'block' : 'none' + outer.innerHTML = ` +
${name}
+
+ ` + const inner = outer.querySelector('.component-wrapper') + let component_content + + // Match sub to factory by component name in type field + component_counters[name] = (component_counters[name] || 0) + 1 + const matching_subs = subs.filter(s => s.type && s.type.endsWith(`>${name}`)) + const occurrence_index = component_counters[name] - 1 + const sub = matching_subs[occurrence_index] + + if (!sub) { + console.error(`No sub found for component: ${name} \n make sure that the imports variable property name is same as the required component name`) + index++ + continue + } + + if (name === 'theme_widget' || name === 'tile_manager') { + component_content = await factory({ ...sub }, io.invite('theme_widget', { up: id })) + } else { + component_content = await factory({ ...sub }, io.invite(name, { up: id })) + } + component_content.className = 'component-content' + + const node_id = admin.status.s2i[sub.sid] + const editor_index = index + 1 + const component_editor = editor_subs[editor_index] ? await editor(editor_subs[editor_index]) : null + inner.append(component_content, component_editor || document.createElement('div')) + + const result = {} + const drive = admin.status.dataset.drive + + const modulepath = node_id.split(':')[0] + const fields = admin.status.db.read_all(['state', modulepath]) + const nodes = Object.keys(fields).filter(is_state_node) + + function is_state_node (field) { return !isNaN(Number(field.split(':').at(-1))) } + + for (const node of nodes) { + result[node] = {} + const datasets = drive.list('', node) + for (const dataset of datasets) { + result[node][dataset] = {} + const files = drive.list(dataset, node) + for (const file of files) { + result[node][dataset][file] = (await drive.get(dataset + file, node)).raw + } + } + } + + if (editor_subs[editor_index]) { + const editor_id = admin.status.a2i[admin.status.s2i[editor_subs[editor_index].sid]] + const port = await item.get(editor_id) + // await sdbio.at(editor_id) + port.postMessage(result) + } + + components_wrapper.appendChild(outer) + wrappers[index] = { outer, inner, name, checkbox_state: is_initially_checked } + index++ + } + } + + function scroll_to_initial_selected () { + if (selected_name_param) { + const index = names.indexOf(selected_name_param) + if (index !== -1 && wrappers[index]) { + const target_wrapper = wrappers[index].outer + if (target_wrapper.style.display !== 'none') { + setTimeout(scroll_to_selected_wrapper, 100) + + function scroll_to_selected_wrapper () { + target_wrapper.scrollIntoView({ behavior: 'auto', block: 'center' }) + clear_selection_highlight() + target_wrapper.style.backgroundColor = '#2e3440' + current_selected_wrapper = target_wrapper + } + } + } + } + } + + function clear_selection_highlight () { + if (current_selected_wrapper) { + current_selected_wrapper.style.backgroundColor = '' + } + current_selected_wrapper = null + } + + function update_url (selected_name = url_params.get('selected')) { + const checked_indices = wrappers.reduce(collect_checked_index, []) + + function collect_checked_index (acc, wrapper_entry, index) { + if (wrapper_entry.checkbox_state) { acc.push(index + 1) } + return acc + } + + const params = new URLSearchParams() + if (checked_indices.length > 0 && checked_indices.length < wrappers.length) { + params.set('checked', JSON.stringify(checked_indices)) + } + const selected_index = names.indexOf(selected_name) + if (selected_name && selected_index !== -1 && wrappers[selected_index].checkbox_state) { + params.set('selected', selected_name) + } + const new_url = `${window.location.pathname}${params.toString() ? '?' + params.toString() : ''}` + window.history.replaceState(null, '', new_url) + } + + function handle_checkbox_change (detail) { + const { index, checked } = detail + if (wrappers[index]) { + wrappers[index].outer.style.display = checked ? 'block' : 'none' + wrappers[index].checkbox_state = checked + update_url() + if (!checked && current_selected_wrapper === wrappers[index].outer) { + clear_selection_highlight() + update_url(null) + } + } + } + + function handle_label_click (detail) { + const { index, name } = detail + if (wrappers[index]) { + const target_wrapper = wrappers[index].outer + if (target_wrapper.style.display === 'none') { + target_wrapper.style.display = 'block' + wrappers[index].checkbox_state = true + } + target_wrapper.scrollIntoView({ behavior: 'smooth', block: 'center' }) + clear_selection_highlight() + target_wrapper.style.backgroundColor = 'lightblue' + current_selected_wrapper = target_wrapper + update_url(name) + } + } + + function handle_select_all_toggle (detail) { + const { selectAll: select_all } = detail + wrappers.forEach(update_wrapper_visibility) + + function update_wrapper_visibility (wrapper_entry) { + wrapper_entry.outer.style.display = select_all ? 'block' : 'none' + wrapper_entry.checkbox_state = select_all + } + + clear_selection_highlight() + update_url(null) + } + + function handle_resize_toggle () { + console.log('handle_resize_toggle', resize_enabled) + resize_enabled = !resize_enabled + drive.put('resize_container/state.json', resize_enabled) + } + + async function onbatch (batch) { + for (const { type, paths } of batch) { + const data = await Promise.all(paths.map(load_path_raw)) + const func = on[type] || fail + func(data, type) + } + + function load_path_raw (path) { return drive.get(path).then(read_drive_file_raw) } + function read_drive_file_raw (file) { return file.raw } + } + function fail (data, type) { console.warn(__filename + 'invalid message', { cause: { data, type } }) } + function inject (data) { sheet.replaceSync(data[0]) } + function update_resize (data) { + console.log('[ update_resize ]', data) + resize_enabled = data + wrappers.forEach(update_wrapper_resize) + + function update_wrapper_resize (wrap) { + const wrapper = wrap.outer.querySelector('.component-wrapper') + if (wrapper) { + wrapper.style.resize = resize_enabled ? 'both' : 'none' + wrapper.style.overflow = resize_enabled ? 'hidden' : 'visible' + } + } + } + async function send_quick_editor_data () { + const roots = admin.status.db.read(['root_datasets']) + const result = {} + roots.forEach(add_root_dataset) + + function add_root_dataset (root_dataset) { + const root = root_dataset.name + result[root] = {} + const inputs = sdb.admin.get_dataset({ root }) || [] + inputs.forEach(add_input_type) + + function add_input_type (type) { + result[root][type] = {} + const datasets = sdb.admin.get_dataset({ root, type }) + + if (!datasets) return + Object.values(datasets).forEach(add_dataset_name) + + function add_dataset_name (dataset_name) { + result[root][type][dataset_name] = {} + const dataset_ids = sdb.admin.get_dataset({ root, type, name: dataset_name }) + dataset_ids.forEach(add_dataset_id) + + function add_dataset_id (dataset_id) { + const files = admin.status.db.read([root, dataset_id]).files || [] + result[root][type][dataset_name][dataset_id] = {} + files.forEach(add_file_data) + + function add_file_data (file_id) { result[root][type][dataset_name][dataset_id][file_id] = admin.status.db.read([root, file_id]) } + } + } + } + } + + if (!editor_subs[0]) return + const editor_id = admin.status.a2i[admin.status.s2i[editor_subs[0].sid]] + const port = await item.get(editor_id) + // await sdbio.at(editor_id) + port.postMessage(result) + } +} +function fallback_module () { + const menuname = 'menu' + const names = [ + 'tile_manager', + 'theme_widget', + 'taskbar', + 'tabsbar', + 'action_bar', + 'program_container', + 'tabs', + 'console_history', + 'actions', + 'tabbed_editor', + 'task_manager', + 'quick_actions', + 'graph_viewer', + 'action_executor', + 'steps_wizard' + ] + const subs = {} + names.forEach(subgen) + subs.helpers = 0 + subs.DOCS = 0 + subs.net_helper = 0 + subs.taskbar = { + $: '', + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + hardcons: 'hardcons', + docs: 'docs' + } + } + subs.tabs = { + $: '', + 0: '', + mapping: { + icons: 'icons', + variables: 'variables', + scroll: 'scroll', + style: 'style', + docs: 'docs', + actions: 'actions' + } + } + subs.program_container = { + $: '', + 0: '', + mapping: { + style: 'style', + flags: 'flags', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + keybinds: 'keybinds', + undo: 'undo', + docs_style: 'docs_style', + docs: 'docs' + } + } + subs.action_executor = { + $: '', + 0: '', + mapping: { + style: 'style', + variables: 'variables', + docs: 'docs', + data: 'data' + } + } + subs.steps_wizard = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.tabsbar = { + $: '', + 0: '', + mapping: { + icons: 'icons', + style: 'style', + docs: 'docs', + actions: 'actions' + } + } + subs.action_bar = { + $: '', + 0: '', + mapping: { + icons: 'icons', + style: 'style', + actions: 'actions', + variables: 'variables', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + } + subs.console_history = { + $: '', + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + docs: 'docs', + actions: 'actions' + } + } + subs.actions = { + $: '', + 0: '', + mapping: { + actions: 'actions', + icons: 'icons', + hardcons: 'hardcons', + style: 'style', + docs: 'docs' + } + } + subs.tabbed_editor = { + $: '', + 0: '', + mapping: { + style: 'style', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + docs: 'docs' + } + } + subs.task_manager = { + $: '', + 0: '', + mapping: { + style: 'style', + count: 'count', + docs: 'docs', + actions: 'actions' + } + } + subs.quick_actions = { + $: '', + 0: '', + mapping: { + style: 'style', + icons: 'icons', + actions: 'actions', + hardcons: 'hardcons', + prefs: 'prefs', + docs: 'docs' + } + } + subs[menuname] = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.quick_editor = { + $: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + subs.theme_widget = { + $: '', + 0: '', + mapping: { + style: 'style', + commands: 'commands', + icons: 'icons', + scroll: 'scroll', + actions: 'actions', + hardcons: 'hardcons', + files: 'files', + highlight: 'highlight', + active_tab: 'active_tab', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + focused: 'focused', + temp_actions: 'temp_actions', + temp_quick_actions: 'temp_quick_actions', + prefs: 'prefs', + variables: 'variables', + data: 'data', + docs_style: 'docs_style', + docs: 'docs' + } + } + subs.graph_viewer = { + $: '', + 0: '', + mapping: { + theme: 'style', + entries: 'entries', + runtime: 'runtime', + mode: 'mode', + flags: 'flags', + keybinds: 'keybinds', + undo: 'undo', + docs: 'docs' + } + } + for (let i = 0; i < Object.keys(subs).length - 1; i++) { + subs.quick_editor[i] = quick_editor$ + } + + return { + _: subs, + drive: { + 'style/': { + 'theme.css': { + raw: ` + .components-wrapper-container { + padding-top: 10px; /* Adjust as needed */ + } + + .component-outer-wrapper { + margin-bottom: 20px; + padding: 0px 0px 10px 0px; + transition: background-color 0.3s ease; + } + + .component-name-label { + background-color:transparent; + padding: 8px 15px; + text-align: center; + font-weight: bold; + color: #333; + } + + .component-wrapper { + width: 95%; + margin: 0 auto; + position: relative; + padding: 15px; + border: 3px solid #666; + resize: none; + overflow: visible; + border-radius: 0px; + background-color: #eceff4; + min-height: 50px; + } + .component-content { + width: 100%; + height: 100%; + } + .toggle-switch { + position: relative; + display: inline-block; + width: 50px; + height: 26px; + } + + .toggle-switch input { + opacity: 0; + width: 0; + height: 0; + } + + .slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: #ccc; + border-radius: 26px; + transition: 0.4s; + } + + .slider::before { + content: ""; + position: absolute; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + border-radius: 50%; + transition: 0.4s; + } + + input:checked + .slider { + background-color: #2196F3; + } + + input:checked + .slider::before { + transform: translateX(24px); + } + .component-wrapper:hover::before { + content: ''; + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + border: 4px solid skyblue; + pointer-events: none; + z-index: 15; + resize: both; + overflow: hidden; + } + .quick-editor { + position: absolute; + z-index: 100; + top: 0; + right: 0; + } + .component-wrapper:hover .quick-editor { + display: block; + } + .component-wrapper > .quick-editor { + display: none; + top: -5px; + right: -10px; + }` + } + }, + 'resize_container/': { + 'state.json': { + raw: 'false' + } + }, + 'icons/': {}, + 'variables/': {}, + 'scroll/': {}, + 'commands/': {}, + 'actions/': {}, + 'hardcons/': {}, + 'files/': {}, + 'highlight/': {}, + 'count/': {}, + 'entries/': {}, + 'active_tab/': {}, + 'runtime/': {}, + 'mode/': {}, + 'data/': {}, + 'flags/': {}, + 'keybinds/': {}, + 'undo/': {}, + 'focused/': {}, + 'temp_actions/': {}, + 'temp_quick_actions/': {}, + 'prefs/': {}, + 'docs_style/': {}, + 'docs/': {} + } + } + function quick_editor$ (args, tools, [quick_editor]) { + const state = quick_editor() + state.net = { + page: {} + } + return state + } + function subgen (name) { + subs[name] = { + $: '', + 0: '', + mapping: { + style: 'style', + docs: 'docs' + } + } + } +} + +function handle_admin_message (msg) { + const { type } = msg + admin_on[type] && admin_on[type]() +} diff --git a/steps_wizard.test.js b/steps_wizard.test.js new file mode 100644 index 0000000..f06f65f --- /dev/null +++ b/steps_wizard.test.js @@ -0,0 +1,145 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest' +import { readFileSync } from 'fs' +import { join } from 'path' +import { createRequire } from 'module' + +const require = createRequire(import.meta.url) +const net = require('./src/node_modules/net_helper/net_helper.js') + +describe('steps_wizard - Protocol Communication', () => { + let mockProtocol + let receivedMessages + let componentSource + + beforeEach(() => { + receivedMessages = [] + + // Read the actual component source code + componentSource = readFileSync( + join(process.cwd(), 'src/node_modules/steps_wizard/steps_wizard.js'), + 'utf-8' + ) + + // Mock protocol: captures onmessage handler and returns send function + mockProtocol = vi.fn().mockImplementation((onmessage) => { + mockProtocol.onmessage = onmessage + return (message) => { + receivedMessages.push(message) + } + }) + }) + + it('should send step_clicked message type in component code', () => { + // Verify the component source contains the correct message type + const hasCorrectMessageType = componentSource.includes("_.up('step_clicked'") + + expect(hasCorrectMessageType).toBe(true) + + if (!hasCorrectMessageType) { + // Show what type it found instead + const typeMatch = componentSource.match(/type:\s*['"]([^'"]+)['"]/g) + console.error('Found types:', typeMatch) + } + }) + + it('should send correct protocol message structure with step_clicked type', () => { + const parent = net('parent_id_456') + const child = net('wizard_instance_123') + parent.io.on.steps_wizard = msg => receivedMessages.push(msg) + child.io.on.up = vi.fn() + child.io.accept(parent.io.invite('steps_wizard', { up: 'parent_id_456' })) + + // Simulate the exact behavior from on_step_click function + const step = { + name: 'Step 1', + type: 'mandatory', + is_completed: false, + component: 'form_input', + data: '' + } + const index = 0 + const steps = [step] + const accessible = true + + // This mimics the actual code in steps_wizard.js line 115: + // _.up('step_clicked', {}, { ...step, index, total_steps: steps.length, is_accessible: accessible }) + const head = child._.up('step_clicked', {}, { ...step, index, total_steps: steps.length, is_accessible: accessible }) + + // Verify message structure + expect(receivedMessages).toHaveLength(1) + expect(receivedMessages[0].type).toBe('step_clicked') + expect(receivedMessages[0].head).toEqual(['wizard_instance_123', 'parent_id_456', 0]) + expect(head).toEqual(['wizard_instance_123', 'parent_id_456', 0]) + expect(receivedMessages[0].data.index).toBe(0) + }) + + it('should handle init_data message from parent', () => { + const onmessage = vi.fn() + mockProtocol(onmessage) + + const testSteps = [ + { name: 'Step 1', type: 'mandatory', is_completed: false }, + { name: 'Step 2', type: 'optional', is_completed: true } + ] + + // Simulate parent sending init_data + if (mockProtocol.onmessage) { + mockProtocol.onmessage({ + head: ['parent_id_456', 'wizard_instance_123', 0], + refs: {}, + type: 'init_data', + data: testSteps + }) + } + + // In real implementation, this would trigger render_steps + expect(testSteps).toHaveLength(2) + expect(testSteps[0].name).toBe('Step 1') + expect(testSteps[1].is_completed).toBe(true) + }) + + it('should have correct can_access logic in component source', () => { + // Verify the actual can_access function exists in the component + const hasCan_accessFunction = componentSource.includes('function can_access') + expect(hasCan_accessFunction).toBe(true) + + // Extract the ACTUAL can_access function from steps_wizard.js + // Using Function constructor to create the function from source + const functionMatch = componentSource.match(/function can_access\s*\(([^)]*)\)\s*{\s*([\s\S]*?)\n\s*return true\s*\n\s*}/m) + + expect(functionMatch).toBeTruthy() // Function must be found + + if (functionMatch) { + const params = functionMatch[1].trim() + const body = functionMatch[2].trim() + + // Create the actual function from the source code + // eslint-disable-next-line no-new-func + const can_access = new Function(params, `${body}\nreturn true`) + + // Now verify it has the CORRECT logic by checking what it actually does + const steps = [ + { name: 'Step 1', type: 'mandatory', is_completed: true }, + { name: 'Step 2', type: 'optional', is_completed: false }, + { name: 'Step 3', type: 'mandatory', is_completed: false } + ] + + // Test the EXPECTED behavior (what the function SHOULD do) + // Step 1 is complete, Step 2 is optional, so Step 3 should be accessible + const step3Result = can_access(2, steps) + + // If this fails, it means the component logic is wrong + expect(step3Result).toBe(true) // Can skip optional step + + // First step always accessible + expect(can_access(0, steps)).toBe(true) + + // Cannot skip incomplete mandatory step + const stepsWithIncomplete = [ + { name: 'Step 1', type: 'mandatory', is_completed: false }, + { name: 'Step 2', type: 'mandatory', is_completed: false } + ] + expect(can_access(1, stepsWithIncomplete)).toBe(false) + } + }) +}) diff --git a/web/page.js b/web/page.js new file mode 100644 index 0000000..35ba19c --- /dev/null +++ b/web/page.js @@ -0,0 +1,19 @@ +const ui_gallery = require('../src/index') +config().then(boot_default_page) + +async function config () { + const html = document.documentElement + const meta = document.createElement('meta') + const font = 'https://fonts.googleapis.com/css?family=Nunito:300,400,700,900|Slackey&display=swap' + const loadFont = `` + html.setAttribute('lang', 'en') + meta.setAttribute('name', 'viewport') + meta.setAttribute('content', 'width=device-width,initial-scale=1.0') + document.head.append(meta) + document.head.insertAdjacentHTML('beforeend', loadFont) + await document.fonts.ready +} + +async function boot_default_page () { + document.body.append(await ui_gallery()) +}