diff --git a/portal/go.mod b/portal/go.mod index 8c19d65..d92ecdc 100644 --- a/portal/go.mod +++ b/portal/go.mod @@ -9,6 +9,8 @@ require ( github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 + github.com/yuin/goldmark v1.7.0 + github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc golang.org/x/crypto v0.21.0 golang.org/x/image v0.15.0 golang.org/x/oauth2 v0.18.0 diff --git a/portal/internal/api/router.go b/portal/internal/api/router.go index 7f2c7a3..0fded3b 100644 --- a/portal/internal/api/router.go +++ b/portal/internal/api/router.go @@ -219,6 +219,15 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler { }) } + // Documentation site + docsHandler, err := web.NewDocsHandler() + if err != nil { + log.Printf("Warning: Failed to initialize docs handler: %v", err) + } else { + r.Handle("/docs", docsHandler) + r.Handle("/docs/*", docsHandler) + } + // Static file servers for packages and assets // Downloads - serve package files with proper headers r.Handle("/downloads/*", http.StripPrefix("/downloads/", diff --git a/portal/internal/web/docs.go b/portal/internal/web/docs.go new file mode 100644 index 0000000..5b0d6ce --- /dev/null +++ b/portal/internal/web/docs.go @@ -0,0 +1,304 @@ +package web + +import ( + "bytes" + "embed" + "html/template" + "io/fs" + "net/http" + "path" + "strings" + + "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting/v2" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" +) + +//go:embed docs/* +var docsFS embed.FS + +// DocsHandler serves documentation pages +type DocsHandler struct { + md goldmark.Markdown + template *template.Template + docs fs.FS +} + +// NewDocsHandler creates a new documentation handler +func NewDocsHandler() (*DocsHandler, error) { + // Configure goldmark with extensions + md := goldmark.New( + goldmark.WithExtensions( + extension.GFM, // GitHub Flavored Markdown + extension.Table, + extension.Strikethrough, + extension.TaskList, + highlighting.NewHighlighting( + highlighting.WithStyle("monokai"), + ), + ), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + ), + goldmark.WithRendererOptions( + html.WithUnsafe(), // Allow raw HTML + ), + ) + + // Create the page template + tmpl, err := template.New("doc").Parse(docPageTemplate) + if err != nil { + return nil, err + } + + // Get embedded docs filesystem + docs, err := fs.Sub(docsFS, "docs") + if err != nil { + return nil, err + } + + return &DocsHandler{ + md: md, + template: tmpl, + docs: docs, + }, nil +} + +// ServeHTTP handles documentation requests +func (h *DocsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Get the requested path + docPath := strings.TrimPrefix(r.URL.Path, "/docs") + if docPath == "" || docPath == "/" { + docPath = "/index" + } + + // Clean the path and add .md extension + docPath = path.Clean(docPath) + if !strings.HasSuffix(docPath, ".md") { + docPath = docPath + ".md" + } + docPath = strings.TrimPrefix(docPath, "/") + + // Read the markdown file + content, err := fs.ReadFile(h.docs, docPath) + if err != nil { + // Try index.md in directory + if !strings.HasSuffix(docPath, "/index.md") { + dirPath := strings.TrimSuffix(docPath, ".md") + "/index.md" + content, err = fs.ReadFile(h.docs, dirPath) + } + if err != nil { + http.NotFound(w, r) + return + } + } + + // Extract title from first heading + title := extractTitle(content) + if title == "" { + title = "Documentation" + } + + // Convert markdown to HTML + var buf bytes.Buffer + if err := h.md.Convert(content, &buf); err != nil { + http.Error(w, "Failed to render documentation", http.StatusInternalServerError) + return + } + + // Build navigation + nav := h.buildNavigation(docPath) + + // Render page + data := docPageData{ + Title: title, + Content: template.HTML(buf.String()), + Navigation: nav, + CurrentPath: "/" + strings.TrimSuffix(docPath, ".md"), + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := h.template.Execute(w, data); err != nil { + http.Error(w, "Template error", http.StatusInternalServerError) + } +} + +type docPageData struct { + Title string + Content template.HTML + Navigation []navSection + CurrentPath string +} + +type navSection struct { + Title string + Items []navItem +} + +type navItem struct { + Title string + Path string + Active bool +} + +// buildNavigation creates the documentation navigation structure +func (h *DocsHandler) buildNavigation(currentPath string) []navSection { + currentPath = "/" + strings.TrimSuffix(currentPath, ".md") + + return []navSection{ + { + Title: "Getting Started", + Items: []navItem{ + {Title: "Introduction", Path: "/docs", Active: currentPath == "/index"}, + {Title: "Quick Start", Path: "/docs/getting-started", Active: currentPath == "/getting-started"}, + {Title: "FAQ", Path: "/docs/faq", Active: currentPath == "/faq"}, + }, + }, + { + Title: "Guides", + Items: []navItem{ + {Title: "UI Design", Path: "/docs/guides/ui-design", Active: currentPath == "/guides/ui-design"}, + {Title: "Lua Scripting", Path: "/docs/guides/lua-scripting", Active: currentPath == "/guides/lua-scripting"}, + {Title: "Permissions", Path: "/docs/guides/permissions", Active: currentPath == "/guides/permissions"}, + {Title: "Best Practices", Path: "/docs/guides/best-practices", Active: currentPath == "/guides/best-practices"}, + }, + }, + { + Title: "Reference", + Items: []navItem{ + {Title: "Lua API", Path: "/docs/api/lua-api", Active: currentPath == "/api/lua-api"}, + {Title: "Manifest", Path: "/docs/api/manifest", Active: currentPath == "/api/manifest"}, + {Title: "CLI", Path: "/docs/cli", Active: currentPath == "/cli"}, + }, + }, + { + Title: "Help", + Items: []navItem{ + {Title: "Troubleshooting", Path: "/docs/troubleshooting", Active: currentPath == "/troubleshooting"}, + }, + }, + } +} + +// extractTitle extracts the first H1 heading from markdown +func extractTitle(content []byte) string { + lines := bytes.Split(content, []byte("\n")) + for _, line := range lines { + line = bytes.TrimSpace(line) + if bytes.HasPrefix(line, []byte("# ")) { + return string(bytes.TrimPrefix(line, []byte("# "))) + } + } + return "" +} + +// docPageTemplate is the HTML template for documentation pages +const docPageTemplate = ` + + + + + {{.Title}} - Mosis Docs + + + + + +
+
+ + + + + Mosis Docs + + +
+
+ +
+ + + + +
+
+ {{.Content}} +
+ + + +
+
+ +` diff --git a/portal/internal/web/docs/api/lua-api.md b/portal/internal/web/docs/api/lua-api.md new file mode 100644 index 0000000..c4a6063 --- /dev/null +++ b/portal/internal/web/docs/api/lua-api.md @@ -0,0 +1,806 @@ +# Lua API Reference + +Complete reference for the Mosis Lua API available to apps. + +## Global Objects + +### document + +The current RML document. Use to query and modify UI elements. + +```lua +-- Get element by ID +local elem = document:GetElementById("my-id") + +-- Get elements by tag +local buttons = document:GetElementsByTagName("button") + +-- Get elements by class +local cards = document:GetElementsByClassName("card") +``` + +### event + +Available in event handler functions. Contains information about the triggering event. + +```lua +function handleClick(event) + local target = event:GetCurrentElement() + local eventType = event.type +end +``` + +## Document Methods + +### GetElementById(id) + +Returns the element with the specified ID, or `nil` if not found. + +```lua +local element = document:GetElementById("username-input") +if element then + element.inner_rml = "Found!" +end +``` + +### GetElementsByTagName(tag) + +Returns a table of all elements with the specified tag name. + +```lua +local buttons = document:GetElementsByTagName("button") +for i, btn in ipairs(buttons) do + btn:SetClass("styled", true) +end +``` + +### GetElementsByClassName(class) + +Returns a table of all elements with the specified class name. + +```lua +local items = document:GetElementsByClassName("list-item") +``` + +### CreateElement(tag) + +Creates a new element with the specified tag name. + +```lua +local div = document:CreateElement("div") +div.inner_rml = "New element" +parent:AppendChild(div) +``` + +### CreateTextNode(text) + +Creates a text node with the specified content. + +```lua +local text = document:CreateTextNode("Hello") +element:AppendChild(text) +``` + +## Element Properties + +### inner_rml + +Gets or sets the inner RML content of an element. + +```lua +-- Get content +local content = element.inner_rml + +-- Set content (parses RML) +element.inner_rml = "Bold text" +``` + +### id + +Gets or sets the element's ID. + +```lua +local id = element.id +element.id = "new-id" +``` + +### style + +Access to the element's inline styles. + +```lua +element.style.width = "100dp" +element.style.backgroundColor = "#ff0000" +element.style.display = "none" +``` + +### parent_node + +Returns the parent element, or `nil` if none. + +```lua +local parent = element.parent_node +``` + +### first_child / last_child + +Returns the first or last child element. + +```lua +local first = container.first_child +local last = container.last_child +``` + +### next_sibling / previous_sibling + +Returns the next or previous sibling element. + +```lua +local next = element.next_sibling +``` + +### child_nodes + +Returns a table of all child elements. + +```lua +local children = element.child_nodes +for i, child in ipairs(children) do + print(child.id) +end +``` + +### tag_name + +Returns the element's tag name (lowercase). + +```lua +local tag = element.tag_name -- "div", "button", etc. +``` + +### offset_width / offset_height + +Returns the rendered dimensions of the element. + +```lua +local width = element.offset_width +local height = element.offset_height +``` + +### offset_left / offset_top + +Returns the position relative to the offset parent. + +```lua +local x = element.offset_left +local y = element.offset_top +``` + +## Element Methods + +### GetAttribute(name) + +Returns the value of the specified attribute. + +```lua +local value = input:GetAttribute("value") +local placeholder = input:GetAttribute("placeholder") +``` + +### SetAttribute(name, value) + +Sets the value of the specified attribute. + +```lua +input:SetAttribute("placeholder", "Enter text...") +button:SetAttribute("disabled", "disabled") +``` + +### RemoveAttribute(name) + +Removes the specified attribute. + +```lua +button:RemoveAttribute("disabled") +``` + +### HasAttribute(name) + +Returns `true` if the element has the specified attribute. + +```lua +if button:HasAttribute("disabled") then + print("Button is disabled") +end +``` + +### SetClass(name, add) + +Adds or removes a class from the element. + +```lua +-- Add class +element:SetClass("active", true) + +-- Remove class +element:SetClass("active", false) +``` + +### IsClassSet(name) + +Returns `true` if the element has the specified class. + +```lua +if element:IsClassSet("selected") then + print("Element is selected") +end +``` + +### AppendChild(element) + +Appends a child element. + +```lua +local child = document:CreateElement("div") +parent:AppendChild(child) +``` + +### InsertBefore(element, reference) + +Inserts an element before the reference element. + +```lua +parent:InsertBefore(newElement, referenceElement) +``` + +### RemoveChild(element) + +Removes a child element. + +```lua +parent:RemoveChild(childElement) +``` + +### Focus() + +Sets focus to the element. + +```lua +input:Focus() +``` + +### Blur() + +Removes focus from the element. + +```lua +input:Blur() +``` + +### Click() + +Simulates a click on the element. + +```lua +button:Click() +``` + +### ScrollIntoView(alignToTop) + +Scrolls the element into view. + +```lua +element:ScrollIntoView(true) -- align to top +element:ScrollIntoView(false) -- align to bottom +``` + +### AddEventListener(event, handler) + +Adds an event listener to the element. + +```lua +button:AddEventListener("click", function(event) + print("Clicked!") +end) +``` + +### RemoveEventListener(event, handler) + +Removes an event listener from the element. + +```lua +local handler = function(event) print("Click") end +button:AddEventListener("click", handler) +button:RemoveEventListener("click", handler) +``` + +## Event Object + +### type + +The event type string (e.g., "click", "change"). + +```lua +if event.type == "click" then + -- handle click +end +``` + +### target_element + +The element that originally triggered the event. + +```lua +local target = event.target_element +``` + +### current_element + +The element the event handler is attached to. + +```lua +local current = event.current_element +``` + +### GetCurrentElement() + +Returns the current element (same as `current_element`). + +```lua +local elem = event:GetCurrentElement() +``` + +### StopPropagation() + +Stops the event from bubbling up to parent elements. + +```lua +event:StopPropagation() +``` + +### StopImmediatePropagation() + +Stops the event and prevents other handlers on the same element. + +```lua +event:StopImmediatePropagation() +``` + +### parameters + +Table containing event-specific parameters. + +```lua +-- Mouse events +local x = event.parameters.mouse_x +local y = event.parameters.mouse_y +local button = event.parameters.button -- 0=left, 1=right, 2=middle + +-- Keyboard events +local key = event.parameters.key_identifier +local ctrl = event.parameters.ctrl_key +local shift = event.parameters.shift_key +local alt = event.parameters.alt_key +``` + +## Navigation + +### navigateTo(screen) + +Navigates to a screen, pushing to history. + +```lua +navigateTo("settings") -- loads assets/settings.rml +navigateTo("screens/profile") -- loads assets/screens/profile.rml +``` + +### goBack() + +Navigates back to the previous screen. + +```lua +goBack() +``` + +### goHome() + +Navigates to the home screen, clearing history. + +```lua +goHome() +``` + +### replaceTo(screen) + +Replaces current screen without adding to history. + +```lua +replaceTo("login") -- no back navigation possible +``` + +### canGoBack() + +Returns `true` if there's a previous screen in history. + +```lua +if canGoBack() then + backButton.style.display = "block" +else + backButton.style.display = "none" +end +``` + +## Timers + +### setTimeout(callback, delay) + +Executes callback once after delay (milliseconds). Returns timer ID. + +```lua +local id = setTimeout(function() + print("Executed after 1 second") +end, 1000) +``` + +### clearTimeout(id) + +Cancels a timeout. + +```lua +local id = setTimeout(callback, 1000) +clearTimeout(id) +``` + +### setInterval(callback, interval) + +Executes callback repeatedly. Returns timer ID. + +```lua +local id = setInterval(function() + updateClock() +end, 1000) +``` + +### clearInterval(id) + +Cancels an interval. + +```lua +clearInterval(intervalId) +``` + +## Storage + +Persistent key-value storage. Data persists between app sessions. + +### storage.set(key, value) + +Stores a value. Value can be string, number, boolean, or table. + +```lua +storage.set("username", "alice") +storage.set("settings", { darkMode = true, fontSize = 16 }) +storage.set("highScore", 1000) +``` + +### storage.get(key) + +Retrieves a stored value, or `nil` if not found. + +```lua +local username = storage.get("username") +local settings = storage.get("settings") +if settings then + print(settings.darkMode) +end +``` + +### storage.remove(key) + +Removes a stored value. + +```lua +storage.remove("tempData") +``` + +### storage.clear() + +Removes all stored values. + +```lua +storage.clear() +``` + +### storage.keys() + +Returns a table of all storage keys. + +```lua +local keys = storage.keys() +for i, key in ipairs(keys) do + print(key) +end +``` + +## HTTP (requires `network` permission) + +### http.get(url, callback) + +Makes a GET request. + +```lua +http.get("https://api.example.com/data", function(response) + if response.ok then + local data = json.decode(response.body) + print(data.message) + else + print("Error: " .. response.status) + end +end) +``` + +### http.post(url, options, callback) + +Makes a POST request. + +```lua +http.post("https://api.example.com/submit", { + headers = { + ["Content-Type"] = "application/json", + ["Authorization"] = "Bearer token123" + }, + body = json.encode({ name = "test" }) +}, function(response) + print("Status: " .. response.status) +end) +``` + +### http.request(options, callback) + +Makes a custom HTTP request. + +```lua +http.request({ + method = "PUT", + url = "https://api.example.com/resource/1", + headers = { ["Content-Type"] = "application/json" }, + body = json.encode({ updated = true }), + timeout = 5000 -- milliseconds +}, function(response) + print(response.status) +end) +``` + +### Response Object + +| Property | Type | Description | +|----------|------|-------------| +| `ok` | boolean | `true` if status is 200-299 | +| `status` | number | HTTP status code | +| `statusText` | string | Status message | +| `headers` | table | Response headers | +| `body` | string | Response body | + +## JSON + +### json.encode(value) + +Converts a Lua value to a JSON string. + +```lua +local str = json.encode({ + name = "Alice", + items = {"a", "b", "c"}, + count = 3 +}) +-- '{"name":"Alice","items":["a","b","c"],"count":3}' +``` + +### json.decode(str) + +Parses a JSON string into a Lua value. + +```lua +local data = json.decode('{"name":"Alice","age":25}') +print(data.name) -- "Alice" +print(data.age) -- 25 +``` + +## Logging + +### print(...) + +Outputs to the debug console. Accepts multiple arguments. + +```lua +print("Debug message") +print("Value:", someValue, "Count:", count) +``` + +### console.log(...) + +Alias for `print()`. + +```lua +console.log("Hello") +``` + +### console.warn(...) + +Logs a warning message. + +```lua +console.warn("Something might be wrong") +``` + +### console.error(...) + +Logs an error message. + +```lua +console.error("Something went wrong:", errorMessage) +``` + +## Utility Functions + +### tostring(value) + +Converts a value to a string. + +```lua +local str = tostring(123) -- "123" +``` + +### tonumber(value) + +Converts a value to a number. + +```lua +local num = tonumber("123") -- 123 +local invalid = tonumber("abc") -- nil +``` + +### type(value) + +Returns the type of a value as a string. + +```lua +type("hello") -- "string" +type(123) -- "number" +type(true) -- "boolean" +type({}) -- "table" +type(nil) -- "nil" +type(print) -- "function" +``` + +### pairs(table) + +Iterator for all key-value pairs. + +```lua +for key, value in pairs(myTable) do + print(key, value) +end +``` + +### ipairs(table) + +Iterator for array elements (integer keys starting from 1). + +```lua +for index, value in ipairs(myArray) do + print(index, value) +end +``` + +### pcall(func, ...) + +Calls a function in protected mode (catches errors). + +```lua +local success, result = pcall(function() + return json.decode(maybeInvalidJson) +end) + +if success then + print("Parsed:", result) +else + print("Error:", result) +end +``` + +## Standard Libraries + +### string + +```lua +string.len(s) -- length +string.upper(s) -- uppercase +string.lower(s) -- lowercase +string.sub(s, i, j) -- substring +string.find(s, pattern) -- find pattern +string.gsub(s, pattern, repl) -- replace +string.match(s, pattern) -- match pattern +string.format(fmt, ...) -- format string +string.byte(s, i) -- character code +string.char(...) -- character from code +string.rep(s, n) -- repeat string +string.reverse(s) -- reverse string +string.split(s, sep) -- split by separator (extension) +string.trim(s) -- trim whitespace (extension) +``` + +### math + +```lua +math.abs(x) -- absolute value +math.ceil(x) -- round up +math.floor(x) -- round down +math.round(x) -- round to nearest (extension) +math.max(...) -- maximum +math.min(...) -- minimum +math.sqrt(x) -- square root +math.pow(x, y) -- power +math.exp(x) -- e^x +math.log(x) -- natural log +math.sin(x) -- sine +math.cos(x) -- cosine +math.tan(x) -- tangent +math.asin(x) -- arc sine +math.acos(x) -- arc cosine +math.atan(x) -- arc tangent +math.atan2(y, x) -- arc tangent of y/x +math.deg(x) -- radians to degrees +math.rad(x) -- degrees to radians +math.random() -- random 0-1 +math.random(n) -- random 1-n +math.random(m, n) -- random m-n +math.randomseed(x) -- set random seed +math.pi -- 3.14159... +math.huge -- infinity +``` + +### table + +```lua +table.insert(t, value) -- append +table.insert(t, pos, value) -- insert at position +table.remove(t) -- remove last +table.remove(t, pos) -- remove at position +table.sort(t) -- sort ascending +table.sort(t, comp) -- sort with comparator +table.concat(t, sep) -- join to string +table.unpack(t) -- unpack to values (extension) +table.pack(...) -- pack values to table (extension) +``` + +### os + +```lua +os.time() -- current timestamp +os.time(t) -- timestamp from table +os.date() -- current date string +os.date(format) -- formatted date +os.date(format, t) -- formatted date for timestamp +os.date("*t") -- date as table +os.difftime(t2, t1) -- time difference +os.clock() -- CPU time used +``` + +Date format codes: +- `%Y` - 4-digit year +- `%m` - month (01-12) +- `%d` - day (01-31) +- `%H` - hour (00-23) +- `%M` - minute (00-59) +- `%S` - second (00-59) +- `%a` - abbreviated weekday +- `%A` - full weekday +- `%b` - abbreviated month +- `%B` - full month + +## See Also + +- [Lua Scripting Guide](../guides/lua-scripting.md) - Tutorials and examples +- [Permissions Guide](../guides/permissions.md) - Permission system +- [UI Design Guide](../guides/ui-design.md) - RML/RCSS reference diff --git a/portal/internal/web/docs/api/manifest.md b/portal/internal/web/docs/api/manifest.md new file mode 100644 index 0000000..244c7c5 --- /dev/null +++ b/portal/internal/web/docs/api/manifest.md @@ -0,0 +1,341 @@ +# Manifest Reference + +Every Mosis app requires a `manifest.json` file in the root of the package. This file describes your app and its requirements. + +## Complete Schema + +```json +{ + "id": "com.example.myapp", + "name": "My App", + "version": "1.0.0", + "version_code": 1, + "entry": "assets/main.rml", + "permissions": [], + "min_mosis_version": "1.0.0", + "author": { + "name": "Developer Name", + "email": "dev@example.com", + "url": "https://example.com" + }, + "icons": { + "32": "icons/icon-32.png", + "64": "icons/icon-64.png", + "128": "icons/icon-128.png" + }, + "description": "A short description of your app", + "category": "utilities", + "screenshots": [ + "screenshots/1.png", + "screenshots/2.png" + ], + "locales": { + "default": "en", + "supported": ["en", "es", "fr"] + } +} +``` + +## Required Fields + +### id + +**Type:** `string` + +Unique package identifier in reverse domain notation. Must match the ID registered in the developer portal. + +```json +"id": "com.yourcompany.appname" +``` + +Rules: +- Lowercase letters, numbers, and periods only +- Must have at least two segments (e.g., `com.app`) +- Maximum 255 characters +- Cannot start or end with a period + +### name + +**Type:** `string` + +Display name shown to users. Maximum 50 characters. + +```json +"name": "My Awesome App" +``` + +### version + +**Type:** `string` + +Human-readable version string following semantic versioning (MAJOR.MINOR.PATCH). + +```json +"version": "1.0.0" +"version": "2.1.3-beta" +``` + +### version_code + +**Type:** `integer` + +Numeric version code that must increase with each release. Used to determine if an update is available. + +```json +"version_code": 1 +``` + +Rules: +- Must be a positive integer +- Must be greater than all previously published versions +- Maximum value: 2147483647 + +### entry + +**Type:** `string` + +Path to the main RML file, relative to package root. + +```json +"entry": "assets/main.rml" +``` + +### author + +**Type:** `object` + +Information about the app developer. + +```json +"author": { + "name": "Developer Name", + "email": "dev@example.com", + "url": "https://example.com" +} +``` + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `name` | string | Yes | Developer or company name | +| `email` | string | Yes | Contact email | +| `url` | string | No | Website URL | + +### icons + +**Type:** `object` + +App icons at various sizes. At minimum, provide a 128px icon. + +```json +"icons": { + "32": "icons/icon-32.png", + "64": "icons/icon-64.png", + "128": "icons/icon-128.png" +} +``` + +Supported sizes: 32, 64, 128, 256, 512 + +Requirements: +- PNG format recommended +- Square aspect ratio +- No transparency on edges (for proper display) + +## Optional Fields + +### description + +**Type:** `string` + +Short description shown in app listings. Maximum 200 characters. + +```json +"description": "A simple calculator for everyday math." +``` + +### permissions + +**Type:** `array` + +List of permissions your app requires. Apps cannot access restricted features without declaring permissions. + +```json +"permissions": ["storage", "network"] +``` + +See [Permissions](#permissions-reference) below. + +### min_mosis_version + +**Type:** `string` + +Minimum Mosis version required to run this app. + +```json +"min_mosis_version": "1.0.0" +``` + +If omitted, defaults to `"1.0.0"`. + +### category + +**Type:** `string` + +App store category for discovery. + +```json +"category": "productivity" +``` + +Valid categories: +- `games` +- `entertainment` +- `productivity` +- `utilities` +- `social` +- `communication` +- `lifestyle` +- `education` +- `health` +- `finance` +- `news` +- `other` + +### screenshots + +**Type:** `array` + +Paths to screenshot images for app store listing. + +```json +"screenshots": [ + "screenshots/home.png", + "screenshots/settings.png", + "screenshots/detail.png" +] +``` + +Requirements: +- PNG format +- 1080x1920 (9:16 portrait) recommended +- Maximum 5 screenshots + +### locales + +**Type:** `object` + +Internationalization configuration. + +```json +"locales": { + "default": "en", + "supported": ["en", "es", "fr", "de", "ja"] +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `default` | string | Default locale code | +| `supported` | array | List of supported locale codes | + +Locale files should be placed in `locales/{code}.json`. + +## Permissions Reference + +| Permission | Description | Example Use | +|------------|-------------|-------------| +| `storage` | Persist data locally | Save user preferences | +| `network` | Make HTTP requests | Fetch remote data | +| `clipboard` | Read/write clipboard | Copy text | +| `notifications` | Show notifications | Reminders | +| `camera` | Access device camera | Photo capture | +| `location` | Get device location | Maps, weather | +| `contacts` | Read contacts | Contact picker | +| `microphone` | Record audio | Voice notes | + +### Permission Declaration + +```json +"permissions": [ + "storage", + "network" +] +``` + +Users are informed of permissions before installing. Request only what you need. + +## Validation + +The package builder validates your manifest. Common errors: + +| Error | Cause | Solution | +|-------|-------|----------| +| `Invalid package ID` | ID doesn't match pattern | Use `com.company.app` format | +| `Missing required field` | Required field omitted | Add the field | +| `Invalid version_code` | Not a positive integer | Use positive number | +| `Icon not found` | Icon path doesn't exist | Check file paths | +| `Invalid permission` | Unknown permission | Use valid permission name | + +## Example: Minimal Manifest + +```json +{ + "id": "com.example.hello", + "name": "Hello World", + "version": "1.0.0", + "version_code": 1, + "entry": "main.rml", + "author": { + "name": "Developer", + "email": "dev@example.com" + }, + "icons": { + "128": "icon.png" + } +} +``` + +## Example: Full Manifest + +```json +{ + "id": "com.acme.calculator", + "name": "ACME Calculator", + "version": "2.1.0", + "version_code": 5, + "entry": "assets/main.rml", + "description": "A powerful calculator with scientific functions.", + "category": "utilities", + "permissions": [ + "storage", + "clipboard" + ], + "min_mosis_version": "1.2.0", + "author": { + "name": "ACME Corp", + "email": "apps@acme.com", + "url": "https://acme.com" + }, + "icons": { + "32": "icons/icon-32.png", + "64": "icons/icon-64.png", + "128": "icons/icon-128.png", + "256": "icons/icon-256.png" + }, + "screenshots": [ + "screenshots/basic.png", + "screenshots/scientific.png", + "screenshots/history.png" + ], + "locales": { + "default": "en", + "supported": ["en", "es", "fr", "de"] + } +} +``` + +## Next Steps + +- [Getting Started](../getting-started.md) - Create your first app +- [Permissions Guide](../guides/permissions.md) - Understanding permissions +- [Publishing Guide](../guides/publishing.md) - Submit your app diff --git a/portal/internal/web/docs/cli.md b/portal/internal/web/docs/cli.md new file mode 100644 index 0000000..997501a --- /dev/null +++ b/portal/internal/web/docs/cli.md @@ -0,0 +1,576 @@ +# CLI Reference + +The Mosis CLI (`mosis`) is a command-line tool for building, testing, and publishing Mosis apps. + +## Installation + +### Windows + +Download the installer from your [Developer Dashboard](/dashboard) or use: + +```powershell +# Using winget +winget install omixlab.mosis-cli + +# Or download directly +curl -o mosis-cli.exe https://dl.omixlab.com/cli/windows/mosis.exe +``` + +### macOS + +```bash +# Using Homebrew +brew install omixlab/tap/mosis-cli + +# Or download directly +curl -fsSL https://dl.omixlab.com/cli/macos/mosis > /usr/local/bin/mosis +chmod +x /usr/local/bin/mosis +``` + +### Linux + +```bash +# Download binary +curl -fsSL https://dl.omixlab.com/cli/linux/mosis > ~/.local/bin/mosis +chmod +x ~/.local/bin/mosis + +# Or using snap +sudo snap install mosis-cli +``` + +## Quick Start + +```bash +# Create a new project +mosis init myapp + +# Build the package +cd myapp +mosis build + +# Test locally +mosis run + +# Login and publish +mosis login +mosis publish +``` + +## Commands + +### mosis init + +Create a new Mosis app project. + +```bash +mosis init [options] +``` + +**Arguments:** +- `name` - Project name (creates directory) + +**Options:** +| Option | Description | +|--------|-------------| +| `--template ` | Use a starter template | +| `--package-id ` | Set package ID | +| `--no-git` | Don't initialize git repo | + +**Templates:** +- `default` - Basic app structure +- `minimal` - Bare minimum files +- `navigation` - Multi-screen with navigation +- `form` - Form handling example +- `list` - Scrollable list example + +**Example:** +```bash +mosis init myapp --template navigation --package-id com.example.myapp +``` + +**Output:** +``` +myapp/ +├── manifest.json +├── icon.png +├── assets/ +│ ├── main.rml +│ └── styles.rcss +└── .mosis/ + └── config.json +``` + +--- + +### mosis build + +Build a `.mosis` package from your project. + +```bash +mosis build [options] +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `-o, --output ` | Output file path | +| `--no-sign` | Skip signing (dev only) | +| `--verbose` | Show detailed output | +| `--validate-only` | Validate without building | + +**Example:** +```bash +mosis build -o dist/myapp.mosis +``` + +**Build Process:** +1. Validates manifest.json +2. Checks all referenced files exist +3. Validates RML/RCSS syntax +4. Creates compressed package +5. Signs with developer key (if available) + +--- + +### mosis validate + +Validate your project without building. + +```bash +mosis validate [options] +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--strict` | Enable strict validation | +| `--fix` | Auto-fix simple issues | + +**Checks performed:** +- Manifest schema validation +- Required files existence +- Icon sizes and formats +- RML/RCSS syntax +- Lua syntax +- Package size limits + +**Example output:** +``` +✓ manifest.json is valid +✓ All required icons present +✓ Entry point exists: assets/main.rml +✓ RML syntax valid (3 files) +✓ RCSS syntax valid (2 files) +✓ Lua syntax valid (1 file) +✓ Package size: 45KB (under 10MB limit) + +Validation passed! +``` + +--- + +### mosis run + +Run your app in the local designer for testing. + +```bash +mosis run [options] +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--device ` | Target device profile | +| `--scale ` | Window scale factor | +| `--hot-reload` | Enable hot reload (default) | +| `--no-hot-reload` | Disable hot reload | + +**Device profiles:** +- `phone` - Standard phone (1080x1920) +- `tablet` - Tablet (1200x1920) +- `watch` - Watch (360x360) + +**Example:** +```bash +mosis run --device phone --scale 0.5 +``` + +--- + +### mosis login + +Authenticate with the developer portal. + +```bash +mosis login [options] +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--token ` | Use API token directly | +| `--browser` | Open browser for OAuth | + +**Interactive login:** +```bash +$ mosis login +Opening browser for authentication... +✓ Logged in as developer@example.com +``` + +**Token login (for CI/CD):** +```bash +mosis login --token YOUR_API_TOKEN +``` + +--- + +### mosis logout + +Log out of the developer portal. + +```bash +mosis logout +``` + +--- + +### mosis publish + +Upload and submit your app for review. + +```bash +mosis publish [options] +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `--notes ` | Release notes | +| `--notes-file ` | Release notes from file | +| `--draft` | Upload as draft (don't submit) | +| `--track ` | Release track (production/beta) | + +**Example:** +```bash +mosis publish --notes "Bug fixes and performance improvements" +``` + +**Process:** +1. Builds package (if needed) +2. Uploads to portal +3. Runs automated validation +4. Submits for review (unless `--draft`) + +--- + +### mosis status + +Check the status of your app submissions. + +```bash +mosis status [app-id] +``` + +**Example output:** +``` +com.example.myapp + +Latest Version: 1.2.0 (code: 5) +Status: In Review +Submitted: 2 hours ago + +Previous Versions: + 1.1.0 (4) - Published + 1.0.0 (1) - Published +``` + +--- + +### mosis keys + +Manage signing keys. + +```bash +mosis keys +``` + +**Subcommands:** + +#### keys generate + +Generate a new signing keypair. + +```bash +mosis keys generate [options] +``` + +| Option | Description | +|--------|-------------| +| `-o, --output ` | Output directory | +| `--name ` | Key name | + +```bash +$ mosis keys generate --name production +Generated keypair: + Private: ~/.mosis/keys/production.key + Public: ~/.mosis/keys/production.pub + +Keep your private key secure! Never share it. +``` + +#### keys register + +Upload your public key to the portal. + +```bash +mosis keys register +``` + +```bash +$ mosis keys register ~/.mosis/keys/production.pub +✓ Key registered successfully + Key ID: k_abc123xyz + Algorithm: Ed25519 +``` + +#### keys list + +List registered keys. + +```bash +$ mosis keys list +ID Name Created Status +k_abc123xyz production 2024-01-15 Active +k_def456uvw development 2024-01-10 Active +``` + +#### keys revoke + +Revoke a registered key. + +```bash +mosis keys revoke +``` + +--- + +### mosis config + +Manage CLI configuration. + +```bash +mosis config +``` + +**Subcommands:** + +#### config get + +Get a configuration value. + +```bash +mosis config get +``` + +#### config set + +Set a configuration value. + +```bash +mosis config set +``` + +#### config list + +List all configuration. + +```bash +$ mosis config list +api_url = https://api.omixlab.com +designer_path = /usr/local/bin/mosis-designer +default_key = production +``` + +**Configuration keys:** +| Key | Description | Default | +|-----|-------------|---------| +| `api_url` | API endpoint | https://api.omixlab.com | +| `designer_path` | Path to designer | (auto-detected) | +| `default_key` | Default signing key | (none) | +| `auto_build` | Build before publish | true | + +--- + +### mosis doctor + +Diagnose common issues with your setup. + +```bash +$ mosis doctor +Checking Mosis CLI installation... + +✓ CLI version: 1.2.0 +✓ Designer found: /usr/local/bin/mosis-designer +✓ Authenticated as: developer@example.com +✓ Signing key configured: production +✓ Network connectivity OK + +All checks passed! +``` + +--- + +### mosis version + +Show CLI version information. + +```bash +$ mosis version +mosis-cli version 1.2.0 +Built: 2024-01-15 +Go: 1.21.5 +``` + +--- + +### mosis help + +Show help for any command. + +```bash +mosis help [command] +mosis --help +``` + +## Environment Variables + +| Variable | Description | +|----------|-------------| +| `MOSIS_API_URL` | Override API endpoint | +| `MOSIS_TOKEN` | API token for authentication | +| `MOSIS_KEY_PATH` | Path to signing key | +| `MOSIS_NO_COLOR` | Disable colored output | +| `MOSIS_DEBUG` | Enable debug logging | + +## Configuration Files + +### Global Config + +Location: `~/.mosis/config.json` + +```json +{ + "api_url": "https://api.omixlab.com", + "default_key": "production", + "auto_build": true +} +``` + +### Project Config + +Location: `.mosis/config.json` (in project root) + +```json +{ + "signing_key": "production", + "build_output": "dist/" +} +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Success | +| 1 | General error | +| 2 | Invalid arguments | +| 3 | Authentication required | +| 4 | Validation failed | +| 5 | Network error | +| 6 | Build failed | + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Publish Mosis App +on: + push: + tags: ['v*'] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Mosis CLI + run: | + curl -fsSL https://dl.omixlab.com/cli/linux/mosis > mosis + chmod +x mosis + sudo mv mosis /usr/local/bin/ + + - name: Build and Publish + env: + MOSIS_TOKEN: ${{ secrets.MOSIS_TOKEN }} + run: | + mosis build + mosis publish --notes "Release ${GITHUB_REF#refs/tags/}" +``` + +### GitLab CI + +```yaml +publish: + image: ubuntu:latest + script: + - curl -fsSL https://dl.omixlab.com/cli/linux/mosis > /usr/local/bin/mosis + - chmod +x /usr/local/bin/mosis + - mosis build + - mosis publish --notes "Release $CI_COMMIT_TAG" + only: + - tags + variables: + MOSIS_TOKEN: $MOSIS_TOKEN +``` + +## Troubleshooting + +### "Command not found" + +Ensure the CLI is in your PATH: +```bash +echo $PATH +which mosis +``` + +### "Authentication failed" + +Re-login: +```bash +mosis logout +mosis login +``` + +### "Build failed: Invalid manifest" + +Run validation for details: +```bash +mosis validate --strict +``` + +### "Network error" + +Check connectivity: +```bash +mosis doctor +curl -I https://api.omixlab.com/health +``` + +## See Also + +- [Getting Started](getting-started.md) - First app tutorial +- [Publishing Guide](guides/publishing.md) - Submission tips +- [API Reference](api/lua-api.md) - Lua API documentation diff --git a/portal/internal/web/docs/faq.md b/portal/internal/web/docs/faq.md new file mode 100644 index 0000000..f60a185 --- /dev/null +++ b/portal/internal/web/docs/faq.md @@ -0,0 +1,267 @@ +# Frequently Asked Questions + +## General + +### What is Mosis? + +Mosis is a virtual smartphone OS for VR games and applications. It provides a phone-like device that users can interact with inside VR environments, complete with real smartphone functionality. + +### Who can develop apps for Mosis? + +Anyone! Sign up for a free developer account to start building apps. There's no fee to register or submit apps. + +### What can I build with Mosis? + +You can build any app that works on a phone screen: +- Utilities (calculators, converters, timers) +- Productivity (notes, to-do lists, calendars) +- Games (puzzles, casual games) +- Entertainment (media players, readers) +- Social apps (chat, messaging) +- And more! + +### How do users get my app? + +Users discover and install apps through the Mosis App Store, which is built into the virtual phone. Published apps appear in store listings where users can browse, search, and install. + +## Development + +### What languages/technologies do I need to know? + +Mosis apps use: +- **RML** - Similar to HTML for structure +- **RCSS** - Similar to CSS for styling +- **Lua** - Lightweight scripting language for logic + +If you know HTML/CSS, you'll find RML/RCSS very familiar. Lua is simple to learn and has many tutorials available. + +### Can I use JavaScript instead of Lua? + +No, Mosis uses Lua for scripting. Lua was chosen for its: +- Lightweight footprint +- Easy sandboxing for security +- Simple learning curve +- Fast execution + +### Can I use React/Vue/Angular? + +No, Mosis uses its own RML/RCSS system based on RmlUi. Standard web frameworks won't work, but the concepts are similar enough that web developers can adapt quickly. + +### What IDEs are supported? + +Use any text editor! VS Code is recommended with these extensions: +- Lua Language Server +- XML/HTML tools for RML editing + +### Is there a visual designer? + +The Desktop Designer provides: +- Live preview of your app +- Hot reload on file changes +- Hierarchy inspection +- Screenshot capture + +It's included in your developer tools download. + +### Can I test on a real device? + +Yes! You can: +1. Install the Designer on your PC +2. Build a .mosis package +3. Sideload onto a VR device with MosisService installed + +### How large can my app be? + +The maximum package size is **10MB**. This is plenty for most apps. If you need more: +- Optimize images (use TGA format) +- Remove unused assets +- Load large data from the network + +### Can I use external APIs? + +Yes, with the `network` permission. Make HTTPS requests to any API: + +```lua +http.get("https://api.example.com/data", function(response) + local data = json.decode(response.body) +end) +``` + +### Can I access the device camera/microphone? + +Yes, with the appropriate permissions: +- `camera` - For photo capture +- `microphone` - For audio recording + +Users will be prompted to grant access. + +### Can my app run in the background? + +Currently, apps only run when visible. Background execution is planned for future versions. + +### Can I access native device features? + +Mosis apps are sandboxed for security. Available device features: +- Storage +- Network +- Camera (with permission) +- Microphone (with permission) +- Clipboard +- Notifications + +Direct hardware access (Bluetooth, USB, etc.) is not available. + +## Publishing + +### How long does review take? + +Most apps are reviewed within 24-48 hours. Apps requesting sensitive permissions may take longer. + +Automated checks run instantly. Manual review is triggered for: +- First-time developers +- Sensitive permissions +- Flagged content + +### Why was my app rejected? + +Check the rejection reason in your dashboard. Common reasons: +- Crashes on launch +- Missing required assets +- Policy violations +- Inappropriate content +- Misleading metadata + +### Can I update my app? + +Yes! Submit a new version with: +- Higher `version_code` +- Updated `version` string +- Release notes + +Updates go through the same review process. + +### Can I remove my app from the store? + +Yes, go to your app's settings and choose "Unpublish". Users who installed it can keep using it, but it won't appear in searches. + +### Can I have paid apps? + +Currently, all apps are free. Paid apps and in-app purchases are planned for future versions. + +### What's the revenue share? + +When monetization launches, the split will be: +- **70%** to developers +- **30%** to Mosis platform + +### Can I distribute outside the store? + +Yes, you can share `.mosis` files directly. However: +- Users must enable sideloading +- Updates won't be automatic +- No store discoverability + +## Technical + +### What RML/RCSS version is supported? + +Mosis uses RmlUi 6.x. The [UI Design Guide](guides/ui-design.md) covers supported features. Not all CSS3 features are available. + +### What Lua version is supported? + +Lua 5.4 with some restrictions for sandboxing. See the [Lua Scripting Guide](guides/lua-scripting.md) for details. + +### Are there size limits for storage? + +Each app has 5MB of local storage. For more data, use network storage. + +### Can I use databases? + +Use the `storage` API for key-value storage. SQLite is not directly available, but you can: +- Store JSON data +- Use a remote database via network + +### How do I handle different screen sizes? + +Design for the standard phone screen (1080x1920 logical pixels). Use: +- `dp` units for consistent sizing +- Flexbox for flexible layouts +- Percentage widths for adaptability + +### Can I create multiple screens? + +Yes, use the navigation system: + +```lua +navigateTo("settings") -- Load settings.rml +goBack() -- Return to previous screen +``` + +### Can apps communicate with each other? + +Currently, apps are isolated. Inter-app communication is planned for future versions. + +### What happens if my app crashes? + +Crashes are caught by the sandbox. The user sees an error message and can restart. Crash reports are sent to your analytics dashboard (if telemetry is enabled). + +### Can I access the file system? + +No direct file system access. Use: +- `storage` API for persisted data +- Bundled assets for static files +- `http` API for remote files + +## Account & Legal + +### Is there a developer fee? + +No, developer accounts are free. There's no cost to register, develop, or publish apps. + +### Can I transfer my app to another developer? + +Contact support to request a transfer. + +### What content is not allowed? + +- Malware or security exploits +- Hate speech or discrimination +- Adult content (unless properly rated) +- Copyright infringement +- Privacy violations +- Impersonation of other apps/brands + +See the full content policy in your developer agreement. + +### Do I need a privacy policy? + +You need a privacy policy if your app: +- Collects user data +- Uses analytics +- Makes network requests +- Accesses contacts, location, etc. + +### Who owns the IP for my app? + +You retain all intellectual property rights to your app. By publishing on Mosis, you grant a license to distribute it through the store. + +### Can I use open source code? + +Yes, but respect the licenses: +- MIT, BSD, Apache: Generally safe +- GPL: May require source distribution +- Proprietary: Check terms carefully + +## More Questions? + +If your question isn't answered here: + +1. Check the [Troubleshooting](troubleshooting.md) guide +2. Search the developer forum +3. Contact support through your dashboard + +## See Also + +- [Getting Started](getting-started.md) - Create your first app +- [Troubleshooting](troubleshooting.md) - Common problems and solutions +- [API Reference](api/lua-api.md) - Complete API documentation diff --git a/portal/internal/web/docs/getting-started.md b/portal/internal/web/docs/getting-started.md new file mode 100644 index 0000000..0090d9f --- /dev/null +++ b/portal/internal/web/docs/getting-started.md @@ -0,0 +1,190 @@ +# Getting Started + +This guide walks you through creating your first Mosis app in under 10 minutes. + +## Prerequisites + +- A Mosis developer account ([sign up here](/register)) +- Text editor (VS Code recommended) +- Desktop Designer for testing (download from portal) + +## Step 1: Create a New App + +1. Log in to the [Developer Portal](/dashboard) +2. Click **Create New App** +3. Fill in the details: + - **Package ID**: `com.yourname.myapp` (unique identifier) + - **App Name**: My First App + - **Description**: A simple hello world app +4. Click **Create** + +## Step 2: Set Up Your Project + +Create a project folder with this structure: + +``` +myapp/ +├── manifest.json +├── icon.png +└── assets/ + ├── main.rml + └── styles.rcss +``` + +### manifest.json + +```json +{ + "id": "com.yourname.myapp", + "name": "My First App", + "version": "1.0.0", + "version_code": 1, + "entry": "assets/main.rml", + "permissions": [], + "min_mosis_version": "1.0.0", + "author": { + "name": "Your Name", + "email": "you@example.com" + }, + "icons": { + "32": "icon.png", + "64": "icon.png", + "128": "icon.png" + } +} +``` + +### assets/main.rml + +```xml + + + My First App + + + +
+

Hello, Mosis!

+

This is my first app.

+ +

Clicks: 0

+
+ + + +
+``` + +### assets/styles.rcss + +```css +body { + font-family: LatoLatin; + background-color: #1a1a2e; + color: #ffffff; +} + +.container { + padding: 20dp; + text-align: center; +} + +h1 { + font-size: 24dp; + margin-bottom: 10dp; + color: #00d4ff; +} + +p { + font-size: 16dp; + margin-bottom: 20dp; +} + +button { + background-color: #00d4ff; + color: #1a1a2e; + padding: 12dp 24dp; + border-radius: 8dp; + font-size: 16dp; + font-weight: bold; +} + +button:hover { + background-color: #00b8e6; +} + +button:active { + background-color: #0099cc; +} + +#counter { + font-size: 18dp; + margin-top: 20dp; +} +``` + +### icon.png + +Create a 128x128 PNG icon for your app. Use any image editor or find a placeholder icon. + +## Step 3: Test Locally + +1. Download and install the Desktop Designer from your dashboard +2. Open a terminal in your project folder +3. Run: + +```bash +mosis-designer.exe assets/main.rml +``` + +The designer window opens showing your app. Changes to RML, RCSS, or Lua files automatically reload. + +## Step 4: Build Your Package + +From your project folder: + +```bash +mosis build +``` + +This creates `myapp.mosis` - your packaged app ready for submission. + +## Step 5: Submit for Review + +1. Go to your app in the Developer Portal +2. Click **Create New Version** +3. Upload your `.mosis` package +4. Add release notes +5. Click **Submit for Review** + +Your app will be reviewed automatically. If it passes all checks, you can publish it to the store. + +## Next Steps + +- [UI Design Guide](guides/ui-design.md) - Learn RML/RCSS in depth +- [Lua Scripting Guide](guides/lua-scripting.md) - Add complex interactivity +- [Permissions Guide](guides/permissions.md) - Request device capabilities +- [Publishing Guide](guides/publishing.md) - Tips for successful submissions + +## Example Apps + +Check out these example apps to learn from: + +| App | Description | Source | +|-----|-------------|--------| +| Calculator | Basic calculator | [View](examples/calculator.md) | +| Notes | Simple note-taking | [View](examples/notes.md) | +| Timer | Countdown timer | [View](examples/timer.md) | + +## Getting Help + +- Join our [Discord community](#) +- Check the [FAQ](faq.md) +- Search the [Troubleshooting guide](troubleshooting.md) diff --git a/portal/internal/web/docs/guides/best-practices.md b/portal/internal/web/docs/guides/best-practices.md new file mode 100644 index 0000000..ad1891d --- /dev/null +++ b/portal/internal/web/docs/guides/best-practices.md @@ -0,0 +1,535 @@ +# Best Practices + +Guidelines for building high-quality Mosis apps that users love. + +## Performance + +### Minimize DOM Queries + +Cache element references instead of querying repeatedly: + +```lua +-- Bad: Queries on every frame +function updateScore() + document:GetElementById("score").inner_rml = tostring(score) +end + +-- Good: Cache the reference +local scoreElement + +function onLoad() + scoreElement = document:GetElementById("score") +end + +function updateScore() + scoreElement.inner_rml = tostring(score) +end +``` + +### Batch DOM Updates + +Group multiple changes together: + +```lua +-- Bad: Multiple separate updates +elem1.style.color = "red" +elem2.style.color = "red" +elem3.style.color = "red" + +-- Good: Use a class +parent:SetClass("error-state", true) +``` + +### Use Efficient Data Structures + +```lua +-- For frequent lookups, use tables as maps +local itemLookup = {} +for i, item in ipairs(items) do + itemLookup[item.id] = item +end + +-- O(1) lookup instead of O(n) search +local item = itemLookup["item-123"] +``` + +### Clean Up Timers + +Always clear intervals when navigating away: + +```lua +local updateInterval + +function onScreenLoad() + updateInterval = setInterval(function() + updateData() + end, 1000) +end + +function onScreenUnload() + if updateInterval then + clearInterval(updateInterval) + updateInterval = nil + end +end +``` + +### Lazy Load Content + +Don't load everything at startup: + +```lua +-- Load data when user scrolls to section +function onSectionVisible(sectionId) + if not loadedSections[sectionId] then + loadSectionData(sectionId) + loadedSections[sectionId] = true + end +end +``` + +## User Experience + +### Provide Feedback + +Show users that actions are happening: + +```lua +function onSubmit() + -- Show loading state immediately + submitButton:SetClass("loading", true) + submitButton:SetAttribute("disabled", "disabled") + + http.post(url, data, function(response) + submitButton:SetClass("loading", false) + submitButton:RemoveAttribute("disabled") + + if response.ok then + showSuccess("Saved!") + else + showError("Failed to save") + end + end) +end +``` + +### Handle Errors Gracefully + +Never show raw error messages to users: + +```lua +http.get(url, function(response) + if response.ok then + displayData(json.decode(response.body)) + else + -- User-friendly message + showMessage("Unable to load data. Please check your connection.") + + -- Log details for debugging + console.error("API error:", response.status, response.body) + end +end) +``` + +### Make Touch Targets Large Enough + +Minimum 48dp for touchable elements: + +```css +.button { + min-width: 48dp; + min-height: 48dp; + padding: 12dp 24dp; +} + +.list-item { + min-height: 56dp; + padding: 16dp; +} +``` + +### Support Undo for Destructive Actions + +```lua +local deletedItem = nil +local undoTimeout = nil + +function deleteItem(itemId) + deletedItem = items[itemId] + items[itemId] = nil + updateList() + + showUndoSnackbar("Item deleted", function() + -- Undo callback + items[itemId] = deletedItem + deletedItem = nil + updateList() + end) + + -- Clear undo after 5 seconds + undoTimeout = setTimeout(function() + deletedItem = nil + permanentlyDelete(itemId) + end, 5000) +end +``` + +### Remember User State + +Restore position and selections when returning: + +```lua +function onScreenUnload() + storage.set("list_scroll_position", scrollContainer.scroll_top) + storage.set("selected_tab", currentTab) +end + +function onScreenLoad() + local scrollPos = storage.get("list_scroll_position") + if scrollPos then + scrollContainer.scroll_top = scrollPos + end + + local tab = storage.get("selected_tab") + if tab then + selectTab(tab) + end +end +``` + +## Code Quality + +### Use Local Variables + +Local variables are faster and prevent global pollution: + +```lua +-- Bad: Global +count = 0 + +-- Good: Local +local count = 0 + +-- Good: Module-level local +local Utils = {} +local cache = {} -- Private to module +``` + +### Handle Edge Cases + +```lua +function divide(a, b) + if b == 0 then + console.warn("Division by zero") + return 0 + end + return a / b +end + +function getUsername(user) + if not user then + return "Unknown" + end + return user.name or user.email or "Unknown" +end +``` + +### Use Meaningful Names + +```lua +-- Bad +local t = {} +local n = 0 + +-- Good +local userScores = {} +local attemptCount = 0 + +-- Bad +function p(x) + return x * 100 +end + +-- Good +function toPercentage(decimal) + return decimal * 100 +end +``` + +### Keep Functions Small + +Each function should do one thing: + +```lua +-- Bad: Does too much +function processUser(userId) + local user = fetchUser(userId) + validateUser(user) + updateUserStats(user) + sendWelcomeEmail(user) + logActivity(user) + return formatUserResponse(user) +end + +-- Good: Composed of small functions +function processNewUser(userId) + local user = fetchUser(userId) + if not isValidUser(user) then + return nil, "Invalid user" + end + initializeUserStats(user) + queueWelcomeEmail(user) + return user +end +``` + +### Comment Why, Not What + +```lua +-- Bad: Describes what (obvious from code) +-- Increment counter by 1 +counter = counter + 1 + +-- Good: Explains why +-- Reset retry count after successful connection +-- to prevent unnecessary backoff on next attempt +retryCount = 0 +``` + +## Security + +### Validate All Input + +```lua +function searchItems(query) + -- Sanitize input + if type(query) ~= "string" then + return {} + end + + query = query:sub(1, 100) -- Limit length + query = query:gsub("[^%w%s]", "") -- Remove special chars + + return performSearch(query) +end +``` + +### Don't Trust External Data + +```lua +http.get(url, function(response) + local success, data = pcall(function() + return json.decode(response.body) + end) + + if not success then + console.error("Invalid JSON from API") + return + end + + -- Validate structure + if type(data.items) ~= "table" then + console.error("Missing items array") + return + end + + processItems(data.items) +end) +``` + +### Never Store Secrets in Code + +```lua +-- Bad: Hardcoded API key +local API_KEY = "sk-12345abcde" + +-- Good: Use environment/config +local apiKey = config.get("api_key") +``` + +### Sanitize Display Content + +When displaying user-generated content, prevent injection: + +```lua +function displayComment(text) + -- Escape HTML entities + text = text:gsub("&", "&") + text = text:gsub("<", "<") + text = text:gsub(">", ">") + + commentElement.inner_rml = text +end +``` + +## Accessibility + +### Use Semantic Elements + +```xml + +
Submit
+ + + + + +

Settings

+

Account

+

Notifications

+``` + +### Provide Text Alternatives + +```xml + +Search + + + +``` + +### Ensure Color Contrast + +Text should have at least 4.5:1 contrast ratio: + +```css +/* Good contrast */ +.light-text { + color: #ffffff; + background-color: #1a1a2e; /* Contrast: 12.6:1 */ +} + +/* Bad contrast */ +.low-contrast { + color: #888888; + background-color: #666666; /* Contrast: 1.3:1 */ +} +``` + +### Don't Rely on Color Alone + +```css +/* Bad: Only color indicates error */ +.error { + color: red; +} + +/* Good: Icon + color + text */ +.error { + color: #ff4444; +} +.error::before { + content: "⚠ "; +} +``` + +## Testing + +### Test Error States + +Don't just test the happy path: + +```lua +-- Test these scenarios: +-- 1. Empty data +-- 2. Network failure +-- 3. Invalid input +-- 4. Timeouts +-- 5. Missing permissions +``` + +### Test Navigation Flows + +Ensure users can: +- Navigate forward and back +- Return to the home screen +- Handle the back button at any screen + +### Test Edge Cases + +- Very long text/names +- Empty lists +- Maximum values +- Rapid repeated actions +- Interrupted operations + +### Use Debug Logging + +```lua +local DEBUG = true + +function debugLog(...) + if DEBUG then + print("[DEBUG]", ...) + end +end + +-- In production build, set DEBUG = false +``` + +## Deployment + +### Use Meaningful Version Numbers + +Follow semantic versioning: +- **MAJOR**: Breaking changes +- **MINOR**: New features, backward compatible +- **PATCH**: Bug fixes + +```json +{ + "version": "2.1.3", + "version_code": 15 +} +``` + +### Write Good Release Notes + +``` +Version 2.1.0 + +New Features: +- Added dark mode support +- New export to PDF feature + +Improvements: +- Faster loading times +- Better error messages + +Bug Fixes: +- Fixed crash when opening empty files +- Fixed date format on some devices +``` + +### Test Before Submitting + +1. Run on the Designer +2. Test all features manually +3. Check on a real device if possible +4. Verify all assets load correctly +5. Test offline behavior + +## Summary Checklist + +Before submitting your app: + +- [ ] All features work as expected +- [ ] Error states are handled gracefully +- [ ] Loading states shown during async operations +- [ ] Touch targets are at least 48dp +- [ ] Text is readable (contrast ratio ≥ 4.5:1) +- [ ] No console errors in normal usage +- [ ] Timers and intervals cleaned up properly +- [ ] User data persists correctly +- [ ] App works after fresh install +- [ ] Version number and code are updated +- [ ] Release notes are meaningful + +## See Also + +- [UI Design Guide](ui-design.md) - Design patterns +- [Lua Scripting Guide](lua-scripting.md) - Code patterns +- [Troubleshooting](../troubleshooting.md) - Common issues diff --git a/portal/internal/web/docs/guides/lua-scripting.md b/portal/internal/web/docs/guides/lua-scripting.md new file mode 100644 index 0000000..289ff11 --- /dev/null +++ b/portal/internal/web/docs/guides/lua-scripting.md @@ -0,0 +1,506 @@ +# Lua Scripting Guide + +Mosis apps use Lua for scripting and interactivity. Each app runs in an isolated sandbox with access to Mosis-specific APIs. + +## Getting Started + +Embed Lua directly in your RML files: + +```xml + + + + + +``` + +Or use external files: + +```xml + +