add documentation site with markdown rendering (M12)

This commit is contained in:
2026-01-18 22:07:35 +01:00
parent 94a573f218
commit 8cb3cf769d
14 changed files with 4847 additions and 0 deletions

View File

@@ -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

View File

@@ -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/",

304
portal/internal/web/docs.go Normal file
View File

@@ -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 = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{.Title}} - Mosis Docs</title>
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* Code highlighting */
pre {
background-color: #1e1e2e;
border-radius: 8px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
}
code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 14px;
}
:not(pre) > code {
background-color: #1e1e2e;
padding: 2px 6px;
border-radius: 4px;
color: #f8f8f2;
}
/* Typography */
.prose h1 { font-size: 2rem; font-weight: 700; margin-top: 2rem; margin-bottom: 1rem; color: #f8fafc; }
.prose h2 { font-size: 1.5rem; font-weight: 600; margin-top: 2rem; margin-bottom: 0.75rem; color: #f8fafc; border-bottom: 1px solid #334155; padding-bottom: 0.5rem; }
.prose h3 { font-size: 1.25rem; font-weight: 600; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #f8fafc; }
.prose h4 { font-size: 1rem; font-weight: 600; margin-top: 1rem; margin-bottom: 0.5rem; color: #f8fafc; }
.prose p { margin-bottom: 1rem; line-height: 1.7; }
.prose a { color: #38bdf8; text-decoration: none; }
.prose a:hover { text-decoration: underline; }
.prose ul, .prose ol { margin-bottom: 1rem; padding-left: 1.5rem; }
.prose li { margin-bottom: 0.5rem; }
.prose ul { list-style-type: disc; }
.prose ol { list-style-type: decimal; }
.prose table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.prose th, .prose td { border: 1px solid #334155; padding: 8px 12px; text-align: left; }
.prose th { background-color: #1e293b; font-weight: 600; }
.prose blockquote { border-left: 4px solid #38bdf8; padding-left: 1rem; margin: 1rem 0; color: #94a3b8; }
.prose hr { border: none; border-top: 1px solid #334155; margin: 2rem 0; }
.prose strong { color: #f8fafc; }
/* Task lists */
.prose input[type="checkbox"] { margin-right: 8px; }
</style>
</head>
<body class="bg-slate-900 text-slate-300 min-h-screen">
<!-- Header -->
<header class="bg-slate-800 border-b border-slate-700 sticky top-0 z-50">
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
<a href="/docs" class="text-xl font-bold text-white flex items-center gap-2">
<svg class="w-8 h-8 text-sky-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/>
</svg>
Mosis Docs
</a>
<nav class="flex items-center gap-6">
<a href="/" class="text-slate-300 hover:text-white transition">Home</a>
<a href="/dashboard" class="text-slate-300 hover:text-white transition">Dashboard</a>
<a href="https://github.com/omixlab/mosis" class="text-slate-300 hover:text-white transition">GitHub</a>
</nav>
</div>
</header>
<div class="max-w-7xl mx-auto flex">
<!-- Sidebar -->
<aside class="w-64 flex-shrink-0 border-r border-slate-700 min-h-[calc(100vh-73px)] sticky top-[73px] self-start hidden lg:block">
<nav class="p-4 space-y-6">
{{range .Navigation}}
<div>
<h3 class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2">{{.Title}}</h3>
<ul class="space-y-1">
{{range .Items}}
<li>
<a href="{{.Path}}" class="block px-3 py-2 rounded-md text-sm transition {{if .Active}}bg-sky-500/10 text-sky-400{{else}}text-slate-300 hover:bg-slate-800{{end}}">
{{.Title}}
</a>
</li>
{{end}}
</ul>
</div>
{{end}}
</nav>
</aside>
<!-- Main content -->
<main class="flex-1 min-w-0">
<article class="prose max-w-4xl mx-auto px-8 py-12">
{{.Content}}
</article>
<!-- Footer -->
<footer class="border-t border-slate-700 px-8 py-6 mt-12">
<div class="max-w-4xl mx-auto flex items-center justify-between text-sm text-slate-400">
<p>&copy; 2024 OmixLab LTD. All rights reserved.</p>
<div class="flex gap-4">
<a href="/privacy" class="hover:text-white transition">Privacy</a>
<a href="/terms" class="hover:text-white transition">Terms</a>
</div>
</div>
</footer>
</main>
</div>
</body>
</html>`

View File

@@ -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 = "<strong>Bold text</strong>"
```
### 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

View File

@@ -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<string>`
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<string>`
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

View File

@@ -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 <name> [options]
```
**Arguments:**
- `name` - Project name (creates directory)
**Options:**
| Option | Description |
|--------|-------------|
| `--template <name>` | Use a starter template |
| `--package-id <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 <path>` | 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 <name>` | Target device profile |
| `--scale <factor>` | 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 <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 <text>` | Release notes |
| `--notes-file <path>` | Release notes from file |
| `--draft` | Upload as draft (don't submit) |
| `--track <name>` | 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 <subcommand>
```
**Subcommands:**
#### keys generate
Generate a new signing keypair.
```bash
mosis keys generate [options]
```
| Option | Description |
|--------|-------------|
| `-o, --output <path>` | Output directory |
| `--name <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 <key-file>
```
```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 <key-id>
```
---
### mosis config
Manage CLI configuration.
```bash
mosis config <subcommand>
```
**Subcommands:**
#### config get
Get a configuration value.
```bash
mosis config get <key>
```
#### config set
Set a configuration value.
```bash
mosis config set <key> <value>
```
#### 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 <command> --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

View File

@@ -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

View File

@@ -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
<rml>
<head>
<title>My First App</title>
<link type="text/rcss" href="styles.rcss"/>
</head>
<body>
<div class="container">
<h1>Hello, Mosis!</h1>
<p>This is my first app.</p>
<button id="click-me" onclick="handleClick()">Click Me</button>
<p id="counter">Clicks: 0</p>
</div>
<script>
local clicks = 0
function handleClick()
clicks = clicks + 1
document:GetElementById("counter").inner_rml = "Clicks: " .. clicks
end
</script>
</body>
</rml>
```
### 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)

View File

@@ -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("&", "&amp;")
text = text:gsub("<", "&lt;")
text = text:gsub(">", "&gt;")
commentElement.inner_rml = text
end
```
## Accessibility
### Use Semantic Elements
```xml
<!-- Bad: Divs for everything -->
<div class="button" onclick="submit()">Submit</div>
<!-- Good: Proper elements -->
<button onclick="submit()">Submit</button>
<!-- Good: Headings create hierarchy -->
<h1>Settings</h1>
<h2>Account</h2>
<h2>Notifications</h2>
```
### Provide Text Alternatives
```xml
<!-- Images should describe their purpose -->
<img src="icons/search.tga" alt="Search"/>
<!-- Icons with meaning need labels -->
<button aria-label="Close">
<img src="icons/close.tga"/>
</button>
```
### 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

View File

@@ -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
<body>
<button onclick="sayHello()">Click Me</button>
<script>
function sayHello()
print("Hello from Lua!")
end
</script>
</body>
```
Or use external files:
```xml
<head>
<script src="scripts/app.lua"/>
</head>
```
## Lua Basics
If you're new to Lua, here's a quick primer:
### Variables
```lua
-- Local variables (preferred)
local name = "Mosis"
local count = 42
local enabled = true
local items = {"apple", "banana", "cherry"}
-- Global variables (avoid when possible)
globalVar = "accessible everywhere"
```
### Functions
```lua
-- Basic function
function greet(name)
return "Hello, " .. name .. "!"
end
-- Function with multiple returns
function getPosition()
return 100, 200
end
local x, y = getPosition()
-- Anonymous functions
local double = function(n) return n * 2 end
```
### Control Flow
```lua
-- If statements
if score > 100 then
print("High score!")
elseif score > 50 then
print("Good job!")
else
print("Keep trying!")
end
-- Loops
for i = 1, 10 do
print(i)
end
for index, value in ipairs(items) do
print(index, value)
end
while condition do
-- loop body
end
```
### Tables
```lua
-- Array-like table
local colors = {"red", "green", "blue"}
print(colors[1]) -- "red" (Lua is 1-indexed)
-- Dictionary-like table
local user = {
name = "Alice",
age = 25,
premium = true
}
print(user.name)
print(user["age"])
-- Mixed table
local app = {
name = "MyApp",
version = "1.0",
features = {"dark mode", "notifications"}
}
```
## DOM Manipulation
Access and modify UI elements using the `document` object:
### Getting Elements
```lua
-- By ID
local button = document:GetElementById("my-button")
-- By tag name
local paragraphs = document:GetElementsByTagName("p")
-- By class name
local cards = document:GetElementsByClassName("card")
```
### Modifying Content
```lua
local element = document:GetElementById("message")
-- Set inner content (HTML-like)
element.inner_rml = "<strong>Hello!</strong>"
-- Get inner content
local content = element.inner_rml
-- Set text only (safer, no HTML parsing)
element:SetInnerRML("Plain text here")
```
### Modifying Attributes
```lua
local input = document:GetElementById("username")
-- Get attribute
local value = input:GetAttribute("value")
-- Set attribute
input:SetAttribute("placeholder", "Enter username")
-- Remove attribute
input:RemoveAttribute("disabled")
```
### Modifying Styles
```lua
local box = document:GetElementById("box")
-- Set individual properties
box.style.width = "200dp"
box.style.backgroundColor = "#00d4ff"
box.style.display = "none" -- hide element
-- Read properties
local width = box.style.width
```
### Classes
```lua
local element = document:GetElementById("panel")
-- Add class
element:SetClass("active", true)
-- Remove class
element:SetClass("active", false)
-- Check class
if element:IsClassSet("active") then
print("Panel is active")
end
```
## Event Handling
### Inline Events
```xml
<button onclick="handleClick()">Click</button>
<input onchange="handleChange(event)"/>
<div onmouseover="handleHover()"/>
```
### Event Listeners
```lua
local button = document:GetElementById("my-button")
-- Add listener
button:AddEventListener("click", function(event)
print("Button clicked!")
end)
-- Remove listener (need reference)
local handler = function(event)
print("Clicked")
end
button:AddEventListener("click", handler)
button:RemoveEventListener("click", handler)
```
### Event Object
```lua
function handleEvent(event)
-- Event type
print(event.type) -- "click", "change", etc.
-- Target element
local target = event:GetCurrentElement()
-- Mouse position (for mouse events)
local x = event.parameters.mouse_x
local y = event.parameters.mouse_y
-- Stop propagation
event:StopPropagation()
end
```
### Common Events
| Event | Description |
|-------|-------------|
| `click` | Element clicked |
| `dblclick` | Element double-clicked |
| `mousedown` | Mouse button pressed |
| `mouseup` | Mouse button released |
| `mouseover` | Mouse enters element |
| `mouseout` | Mouse leaves element |
| `focus` | Element gains focus |
| `blur` | Element loses focus |
| `change` | Input value changed |
| `submit` | Form submitted |
| `keydown` | Key pressed |
| `keyup` | Key released |
## Timers
### setTimeout
```lua
-- Execute once after delay
local timerId = setTimeout(function()
print("Executed after 1 second")
end, 1000) -- milliseconds
-- Cancel timer
clearTimeout(timerId)
```
### setInterval
```lua
-- Execute repeatedly
local intervalId = setInterval(function()
print("Tick")
end, 1000)
-- Cancel interval
clearInterval(intervalId)
```
## Storage
Persist data between app sessions:
```lua
-- Save data
storage.set("username", "Alice")
storage.set("settings", {
darkMode = true,
notifications = false
})
-- Load data
local username = storage.get("username")
local settings = storage.get("settings")
-- Delete data
storage.remove("username")
-- Clear all data
storage.clear()
```
## Navigation
Navigate between screens in your app:
```lua
-- Navigate to screen
navigateTo("settings") -- loads assets/settings.rml
-- Go back
goBack()
-- Go to home screen
goHome()
-- Replace current screen (no back)
replaceTo("login")
```
### Navigation Events
```lua
-- Listen for navigation
onNavigate(function(screenName)
print("Navigated to: " .. screenName)
end)
-- Listen for back
onBack(function()
print("Going back")
end)
```
## HTTP Requests
Make network requests (requires `network` permission):
```lua
-- GET request
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)
-- POST request
http.post("https://api.example.com/submit", {
headers = {
["Content-Type"] = "application/json"
},
body = json.encode({
name = "Alice",
action = "subscribe"
})
}, function(response)
print("Status: " .. response.status)
end)
```
## JSON
```lua
-- Parse JSON string
local data = json.decode('{"name": "Alice", "age": 25}')
print(data.name)
-- Convert to JSON string
local str = json.encode({
items = {"a", "b", "c"},
count = 3
})
```
## Date and Time
```lua
-- Current timestamp
local now = os.time()
-- Format date
local formatted = os.date("%Y-%m-%d %H:%M:%S", now)
-- Parse date components
local t = os.date("*t", now)
print(t.year, t.month, t.day, t.hour, t.min, t.sec)
```
## Utilities
### String Functions
```lua
-- Concatenation
local greeting = "Hello, " .. name .. "!"
-- String functions
string.upper("hello") -- "HELLO"
string.lower("HELLO") -- "hello"
string.sub("hello", 1, 3) -- "hel"
string.find("hello", "ll") -- 3
string.gsub("hello", "l", "L") -- "heLLo"
string.format("Score: %d", 100) -- "Score: 100"
```
### Math Functions
```lua
math.floor(3.7) -- 3
math.ceil(3.2) -- 4
math.round(3.5) -- 4
math.abs(-5) -- 5
math.min(1, 2, 3) -- 1
math.max(1, 2, 3) -- 3
math.random() -- 0-1
math.random(1, 6) -- 1-6
```
### Table Functions
```lua
-- Insert
table.insert(items, "new item")
table.insert(items, 1, "at beginning")
-- Remove
table.remove(items) -- remove last
table.remove(items, 1) -- remove first
-- Sort
table.sort(items)
table.sort(items, function(a, b) return a > b end) -- descending
-- Length
local count = #items
```
## Sandbox Restrictions
For security, these are **NOT** available:
- `os.execute`, `io.popen` - No shell commands
- `loadfile`, `dofile` - No arbitrary file loading
- `require` - No external modules (use `import` for app modules)
- `debug` library - No debugging hooks
- `rawget`, `rawset` - No metatable bypass
## Best Practices
1. **Use local variables** - Faster and prevents pollution
2. **Handle errors** - Use `pcall` for operations that might fail
3. **Clean up timers** - Clear intervals when navigating away
4. **Minimize DOM queries** - Cache element references
5. **Batch updates** - Group style changes together
### Error Handling
```lua
local success, result = pcall(function()
-- Code that might fail
local data = json.decode(invalidJson)
return data
end)
if success then
print("Parsed:", result)
else
print("Error:", result)
end
```
### Module Pattern
```lua
-- utils.lua
local Utils = {}
function Utils.formatCurrency(amount)
return string.format("$%.2f", amount)
end
function Utils.capitalize(str)
return str:sub(1,1):upper() .. str:sub(2)
end
return Utils
```
```lua
-- main.lua
local Utils = import("utils")
print(Utils.formatCurrency(19.99))
```
## Next Steps
- [Permissions Guide](permissions.md) - Request device capabilities
- [API Reference](../api/lua-api.md) - Complete API documentation
- [Debugging Guide](debugging.md) - Debug your Lua code

View File

@@ -0,0 +1,396 @@
# Permissions Guide
Mosis apps run in a secure sandbox with limited access to device features. To access sensitive capabilities, apps must declare permissions in their manifest.
## Why Permissions?
Permissions protect user privacy and security by:
1. **Informing users** what an app can access before installation
2. **Limiting damage** if an app misbehaves
3. **Maintaining trust** in the Mosis ecosystem
## Declaring Permissions
Add permissions to your `manifest.json`:
```json
{
"id": "com.example.myapp",
"name": "My App",
"permissions": [
"storage",
"network"
]
}
```
Only request permissions your app actually needs. Users are more likely to trust apps with fewer permissions.
## Available Permissions
### storage
**Description:** Persist data locally between app sessions.
**Use cases:**
- Save user preferences
- Cache data for offline use
- Store app state
**API access:**
```lua
storage.set("key", value)
storage.get("key")
storage.remove("key")
storage.clear()
```
**Note:** All apps have access to in-memory storage during a session. The `storage` permission enables persistence across sessions.
---
### network
**Description:** Make HTTP/HTTPS requests to external servers.
**Use cases:**
- Fetch data from APIs
- Submit form data
- Load remote content
**API access:**
```lua
http.get(url, callback)
http.post(url, options, callback)
http.request(options, callback)
```
**Restrictions:**
- HTTPS only (HTTP blocked for security)
- Cannot access localhost or internal IPs
- Subject to CORS policies
---
### clipboard
**Description:** Read from and write to the system clipboard.
**Use cases:**
- Copy text or data
- Paste user content
- Share functionality
**API access:**
```lua
clipboard.write(text)
clipboard.read(callback)
```
---
### notifications
**Description:** Display system notifications to the user.
**Use cases:**
- Reminders
- Alerts
- Background updates
**API access:**
```lua
notifications.show({
title = "Reminder",
body = "Your timer is done!",
icon = "icons/alarm.png"
})
```
**Restrictions:**
- Notifications may be rate-limited
- Users can disable notifications per-app
---
### camera
**Description:** Capture photos using the device camera.
**Use cases:**
- Photo capture
- QR code scanning
- Augmented reality
**API access:**
```lua
camera.capture({
quality = "high",
facing = "back"
}, function(result)
if result.success then
local imageData = result.data
end
end)
```
**Restrictions:**
- User prompt before first access
- Cannot record video (photo only)
---
### microphone
**Description:** Record audio from the device microphone.
**Use cases:**
- Voice notes
- Audio messages
- Voice commands
**API access:**
```lua
microphone.start()
microphone.stop(function(result)
local audioData = result.data
end)
```
**Restrictions:**
- User prompt before first access
- Maximum recording duration enforced
---
### location
**Description:** Access device location information.
**Use cases:**
- Weather apps
- Maps
- Location-based features
**API access:**
```lua
location.get(function(result)
if result.success then
print(result.latitude, result.longitude)
end
end)
location.watch(function(result)
-- Called on location changes
end)
```
**Restrictions:**
- User prompt before first access
- Approximate location only (no precise GPS)
- Battery impact warning
---
### contacts
**Description:** Read device contacts.
**Use cases:**
- Contact picker
- Address book integration
- Sharing with friends
**API access:**
```lua
contacts.pick(function(result)
if result.success then
print(result.name, result.phone)
end
end)
contacts.getAll(function(result)
for i, contact in ipairs(result.contacts) do
print(contact.name)
end
end)
```
**Restrictions:**
- Read-only access
- User prompt before first access
## Permission Levels
| Level | Description | Example |
|-------|-------------|---------|
| **Normal** | Low risk, minimal review | storage |
| **Sensitive** | Requires user prompt | camera, microphone, location |
| **Dangerous** | Extensive review required | contacts |
## Runtime Behavior
### First-Time Prompts
Some permissions trigger a user prompt on first use:
```lua
-- First call triggers prompt
camera.capture(options, function(result)
if result.denied then
-- User denied permission
showPermissionExplanation()
elseif result.success then
-- Permission granted
handlePhoto(result.data)
end
end)
```
### Checking Permission Status
```lua
-- Check if permission is granted
if permissions.check("camera") then
-- Already have permission
showCameraButton()
else
-- Need to request
showRequestButton()
end
```
### Requesting at Runtime
```lua
permissions.request("camera", function(granted)
if granted then
startCamera()
else
showAlternative()
end
end)
```
## Best Practices
### 1. Minimize Permissions
Only request what you need. An app with fewer permissions:
- Builds more user trust
- Passes review faster
- Has smaller attack surface
### 2. Request at the Right Time
Don't request all permissions at startup. Request when the user takes an action that needs it:
```lua
-- Bad: Request on app start
function onAppStart()
permissions.request("camera") -- Why?
end
-- Good: Request when needed
function onTakePhotoClicked()
permissions.request("camera", function(granted)
if granted then
camera.capture(options, handlePhoto)
end
end)
end
```
### 3. Explain Why
Tell users why you need a permission before requesting:
```xml
<div id="permission-explanation" style="display: none;">
<p>This app needs camera access to scan QR codes.</p>
<button onclick="requestCamera()">Enable Camera</button>
</div>
```
### 4. Handle Denial Gracefully
Apps should work (with reduced functionality) even if permissions are denied:
```lua
function capturePhoto()
if not permissions.check("camera") then
-- Offer alternative
showManualEntryOption()
return
end
-- Proceed with camera
end
```
### 5. Don't Ask Again Immediately
If a user denies a permission, don't immediately ask again:
```lua
local lastDenied = storage.get("camera_denied_time")
if lastDenied and os.time() - lastDenied < 86400 then
-- Wait at least 24 hours before asking again
return
end
```
## Review Impact
Permission requests affect app review:
| Permission | Review Impact |
|------------|---------------|
| storage, network | Automatic approval |
| clipboard | Quick review |
| notifications | Standard review |
| camera, microphone | Extended review |
| location | Extended review |
| contacts | Manual review required |
Apps requesting sensitive permissions must:
1. Justify the need in submission notes
2. Use the permission appropriately
3. Respect user privacy
## Troubleshooting
### "Permission not declared"
```
Error: Cannot use camera without 'camera' permission
```
Add the permission to your manifest:
```json
"permissions": ["camera"]
```
### "Permission denied by user"
Handle this gracefully in your code:
```lua
if result.denied then
showAlternativeUI()
end
```
### "Permission blocked"
The user permanently blocked the permission. Direct them to settings:
```lua
if result.blocked then
showMessage("Please enable camera in system settings")
end
```
## See Also
- [Manifest Reference](../api/manifest.md) - Full manifest documentation
- [Security Guide](security.md) - App security best practices
- [Publishing Guide](publishing.md) - App review process

View File

@@ -0,0 +1,395 @@
# UI Design Guide
Mosis uses RML (RmlUi Markup Language) and RCSS (RmlUi CSS) for building user interfaces. If you know HTML and CSS, you'll feel right at home.
## RML Basics
RML is similar to HTML but with some differences optimized for UI rendering.
### Document Structure
```xml
<rml>
<head>
<title>App Title</title>
<link type="text/rcss" href="styles.rcss"/>
</head>
<body>
<!-- Your UI here -->
</body>
</rml>
```
### Common Elements
| Element | Usage |
|---------|-------|
| `<div>` | Container/layout |
| `<p>` | Paragraph text |
| `<span>` | Inline text |
| `<h1>` - `<h6>` | Headings |
| `<img>` | Images |
| `<button>` | Clickable buttons |
| `<input>` | Text input fields |
| `<select>` | Dropdown menus |
| `<progress>` | Progress bars |
### Layout Example
```xml
<div class="app-bar">
<div class="app-bar-nav" onclick="goBack()">
<img src="../../icons/back.tga"/>
</div>
<span class="app-bar-title">My App</span>
</div>
<div class="content">
<div class="card">
<h2>Welcome</h2>
<p>This is a card component.</p>
</div>
</div>
<div class="dock">
<button class="dock-item" onclick="navigateTo('home')">
<img src="icons/home.tga"/>
</button>
</div>
```
## RCSS Styling
RCSS is CSS with some limitations and extensions.
### Supported Properties
**Layout:**
- `display` (block, inline, inline-block, flex, none)
- `position` (static, relative, absolute, fixed)
- `width`, `height`, `min-width`, `max-width`, `min-height`, `max-height`
- `margin`, `padding` (including directional variants)
- `flex`, `flex-direction`, `flex-wrap`, `justify-content`, `align-items`
- `overflow` (visible, hidden, scroll, auto)
**Visual:**
- `background-color`, `background` (with decorators)
- `color`
- `border`, `border-radius`
- `opacity`
- `box-shadow` (via decorators)
**Typography:**
- `font-family`
- `font-size`
- `font-weight` (normal, bold)
- `font-style` (normal, italic)
- `text-align` (left, center, right)
- `line-height`
- `text-decoration`
### Units
| Unit | Description |
|------|-------------|
| `dp` | Density-independent pixels (recommended) |
| `px` | Pixels |
| `%` | Percentage of parent |
| `em` | Relative to font size |
Always use `dp` for consistent sizing across devices:
```css
.button {
padding: 12dp 24dp;
font-size: 16dp;
border-radius: 8dp;
}
```
### Colors
```css
/* Hex colors */
color: #ffffff;
color: #fff;
color: #00d4ff80; /* with alpha */
/* RGB/RGBA */
color: rgb(255, 255, 255);
color: rgba(0, 212, 255, 0.5);
```
### Pseudo-classes
```css
button {
background-color: #00d4ff;
}
button:hover {
background-color: #00b8e6;
}
button:active {
background-color: #0099cc;
}
button:focus {
border: 2dp solid #ffffff;
}
button:disabled {
opacity: 0.5;
}
```
## Flexbox Layout
RCSS supports flexbox for modern layouts:
```css
.row {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.column {
display: flex;
flex-direction: column;
gap: 10dp;
}
.grow {
flex: 1;
}
```
```xml
<div class="row">
<span>Left</span>
<span class="grow"></span>
<span>Right</span>
</div>
```
## Images
Images should be in TGA format for best performance:
```xml
<img src="icons/star.tga"/>
```
Supported formats:
- TGA (recommended)
- PNG
- JPEG
### Image Sizing
```css
img {
width: 32dp;
height: 32dp;
}
/* Aspect ratio maintained */
img.icon {
width: 24dp;
height: auto;
}
```
## Input Elements
### Text Input
```xml
<input type="text" id="username" placeholder="Enter username"/>
```
```css
input {
width: 100%;
padding: 12dp;
background-color: #2a2a4e;
border: 1dp solid #3a3a5e;
border-radius: 8dp;
color: #ffffff;
font-size: 16dp;
}
input:focus {
border-color: #00d4ff;
}
```
### Select/Dropdown
```xml
<select id="country">
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
<option value="ca">Canada</option>
</select>
```
### Progress Bar
```xml
<progress id="loading" value="0.5" max="1"/>
```
```css
progress {
width: 100%;
height: 8dp;
background-color: #2a2a4e;
border-radius: 4dp;
}
progress fill {
background-color: #00d4ff;
border-radius: 4dp;
}
```
## Scrolling
```xml
<div class="scroll-container">
<div class="scroll-content">
<!-- Long content here -->
</div>
</div>
```
```css
.scroll-container {
height: 300dp;
overflow: auto;
}
```
## Decorators
RCSS uses decorators for advanced visual effects:
```css
/* Gradient background */
.gradient {
decorator: horizontal-gradient(#1a1a2e #2a2a4e);
}
/* Image background */
.card {
decorator: image(background.tga);
}
/* Border image */
.fancy-border {
decorator: ninepatch(border.tga, 10dp, 10dp, 10dp, 10dp);
}
```
## Animations
RCSS supports keyframe animations:
```css
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fade-in 0.3s ease-out;
}
```
### Transitions
```css
.button {
transition: background-color 0.2s, transform 0.1s;
}
.button:hover {
background-color: #00b8e6;
}
.button:active {
transform: scale(0.95);
}
```
## Responsive Design
Design for the Mosis phone screen (1080x1920 logical pixels):
```css
/* Base styles for portrait */
.content {
padding: 16dp;
}
/* Adjust for available space */
.app-bar {
height: 56dp;
padding: 0 16dp;
}
.dock {
height: 64dp;
padding: 8dp;
}
```
## Design Tokens
Use CSS variables for consistent theming:
```css
:root {
--primary: #00d4ff;
--primary-dark: #00b8e6;
--background: #1a1a2e;
--surface: #2a2a4e;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--spacing-sm: 8dp;
--spacing-md: 16dp;
--spacing-lg: 24dp;
--radius-sm: 4dp;
--radius-md: 8dp;
--radius-lg: 16dp;
}
.button {
background-color: var(--primary);
padding: var(--spacing-md);
border-radius: var(--radius-md);
}
```
## Best Practices
1. **Use dp units** - Ensures consistent sizing across devices
2. **Test touch targets** - Minimum 48dp for touchable elements
3. **Maintain contrast** - Ensure text is readable (4.5:1 ratio minimum)
4. **Use semantic structure** - Proper headings, lists, etc.
5. **Optimize images** - Use TGA format, appropriate sizes
6. **Keep it simple** - Mobile-first design, avoid clutter
## Next Steps
- [Lua Scripting Guide](lua-scripting.md) - Add interactivity
- [Components Library](components.md) - Pre-built UI components
- [Theme Reference](theme.md) - Complete theming guide

View File

@@ -0,0 +1,51 @@
# Mosis Developer Documentation
Welcome to the Mosis developer documentation. Mosis is a virtual smartphone OS for VR games and applications, providing a phone-like device that users can interact with inside VR environments.
## Quick Links
- [Getting Started](getting-started.md) - Create your first Mosis app
- [UI Guide](guides/ui-design.md) - Design beautiful interfaces with RML/RCSS
- [Lua Scripting](guides/lua-scripting.md) - Add interactivity with Lua
- [API Reference](api/lua-api.md) - Complete Lua API documentation
- [Manifest Reference](api/manifest.md) - App manifest schema
## What is Mosis?
Mosis provides a virtual phone interface for VR applications. Developers can create apps that run inside this virtual phone, offering users familiar smartphone experiences within VR games.
### Key Features
- **RML/RCSS UI** - HTML/CSS-like markup for building interfaces
- **Lua Scripting** - Lightweight scripting for app logic
- **Sandboxed Execution** - Secure isolation per app
- **Cross-Platform** - Works with Unity, Unreal Engine, and more
### Architecture
```
┌─────────────────────────────────────────┐
│ Your VR Game/App │
│ (Unity, Unreal, native Android) │
└──────────────────┬──────────────────────┘
┌──────────────────▼──────────────────────┐
│ MosisService │
│ ┌─────────────────────────────────┐ │
│ │ Your Mosis App │ │
│ │ ┌─────────┐ ┌─────────────┐ │ │
│ │ │ RML/CSS │ │ Lua Scripts │ │ │
│ │ └─────────┘ └─────────────┘ │ │
│ └─────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
## Getting Help
- [Troubleshooting](troubleshooting.md) - Common issues and solutions
- [FAQ](faq.md) - Frequently asked questions
- [API Status](https://status.omixlab.com) - Service status page
## Contributing
Mosis is developed by OmixLab LTD. For questions or feedback, contact us through the developer portal.

View File

@@ -0,0 +1,469 @@
# Troubleshooting
Solutions for common issues when developing Mosis apps.
## Build Errors
### "Invalid manifest: missing required field"
Your `manifest.json` is missing a required field.
**Solution:** Check these required fields:
```json
{
"id": "com.example.app",
"name": "App Name",
"version": "1.0.0",
"version_code": 1,
"entry": "assets/main.rml",
"author": {
"name": "Your Name",
"email": "you@example.com"
},
"icons": {
"128": "icon.png"
}
}
```
### "Invalid package ID format"
Package IDs must follow reverse domain notation.
**Valid:**
- `com.example.myapp`
- `com.yourname.calculator`
- `io.github.user.app`
**Invalid:**
- `myapp` (needs domain prefix)
- `Com.Example.App` (must be lowercase)
- `com..app` (no double dots)
- `com.app.` (can't end with dot)
### "Entry point not found"
The `entry` file specified in manifest doesn't exist.
**Solution:** Verify the path:
```json
"entry": "assets/main.rml"
```
Check that `assets/main.rml` exists relative to your manifest file.
### "Icon not found"
An icon file specified in manifest doesn't exist.
**Solution:**
1. Check file paths are correct
2. Ensure files exist
3. Use forward slashes in paths
```json
"icons": {
"128": "icons/icon-128.png"
}
```
### "Package too large"
Package exceeds the 10MB limit.
**Solutions:**
- Compress images (use TGA or optimized PNG)
- Remove unused assets
- Move large files to external CDN
- Check for accidentally included files
## Runtime Errors
### "attempt to index nil value"
You're accessing a property on a nil variable.
**Common causes:**
1. **Element not found:**
```lua
-- Bad
local elem = document:GetElementById("typo")
elem.inner_rml = "Hello" -- Error: elem is nil
-- Good
local elem = document:GetElementById("correct-id")
if elem then
elem.inner_rml = "Hello"
end
```
2. **Table key doesn't exist:**
```lua
-- Bad
local data = json.decode(response.body)
print(data.user.name) -- Error if user is nil
-- Good
if data and data.user then
print(data.user.name)
end
```
### "attempt to call nil value"
You're calling a function that doesn't exist.
**Common causes:**
1. **Typo in function name:**
```lua
-- Bad: navigateto (lowercase t)
navigateto("settings")
-- Good
navigateTo("settings")
```
2. **Missing permission:**
```lua
-- Error if 'network' permission not declared
http.get(url, callback)
```
### "Permission denied"
You're using an API without the required permission.
**Solution:** Add permission to manifest:
```json
"permissions": ["storage", "network"]
```
### "Network request failed"
HTTP request couldn't complete.
**Common causes:**
1. **No network permission:**
```json
"permissions": ["network"]
```
2. **Invalid URL:**
```lua
-- Bad: missing protocol
http.get("api.example.com/data", callback)
-- Good
http.get("https://api.example.com/data", callback)
```
3. **HTTP not allowed (HTTPS only):**
```lua
-- Bad
http.get("http://example.com/data", callback)
-- Good
http.get("https://example.com/data", callback)
```
4. **CORS error:** The server doesn't allow cross-origin requests. Contact the API provider or use a CORS proxy.
### "Storage quota exceeded"
You've exceeded the 5MB storage limit.
**Solution:**
- Clear unnecessary data: `storage.clear()`
- Use selective removal: `storage.remove("large-key")`
- Store only essential data
- Consider using network storage for large data
## UI Issues
### Element not displaying
**Check:**
1. **Display not set to none:**
```css
/* Element might be hidden */
.element {
display: none; /* Remove this */
}
```
2. **Size is zero:**
```css
.element {
width: 0; /* Add dimensions */
height: 0;
}
```
3. **Element is off-screen:**
```css
.element {
position: absolute;
left: -1000dp; /* Move to visible area */
}
```
4. **Z-index issues:**
```css
.element {
z-index: 1; /* Bring to front */
}
```
### Click events not working
**Check:**
1. **Function exists:**
```xml
<button onclick="handleClick()">Click</button>
```
```lua
-- Make sure function is defined
function handleClick()
print("Clicked!")
end
```
2. **Element is overlapped:**
Another element might be blocking clicks. Check z-index and position.
3. **Element has pointer-events: none:**
```css
.element {
/* Remove this */
pointer-events: none;
}
```
### Styles not applying
**Check:**
1. **Stylesheet is linked:**
```xml
<head>
<link type="text/rcss" href="styles.rcss"/>
</head>
```
2. **Selector is correct:**
```css
/* Class selector needs dot */
.my-class { }
/* ID selector needs hash */
#my-id { }
/* Tag selector has no prefix */
button { }
```
3. **Specificity issues:**
More specific selectors override less specific ones:
```css
/* Less specific */
button { color: blue; }
/* More specific - wins */
.btn.primary { color: red; }
```
4. **Units are correct:**
```css
/* Use dp units */
padding: 12dp;
/* Not px on mobile */
padding: 12px; /* May not work correctly */
```
### Layout breaks on different screens
**Solutions:**
1. **Use dp units instead of px:**
```css
padding: 16dp; /* Scales properly */
```
2. **Use flexbox:**
```css
.container {
display: flex;
flex-direction: column;
}
```
3. **Use percentage widths:**
```css
.card {
width: 90%;
max-width: 400dp;
}
```
### Text is cut off
**Solutions:**
1. **Allow wrapping:**
```css
.text {
word-break: break-word;
}
```
2. **Add overflow scrolling:**
```css
.container {
overflow: auto;
}
```
3. **Use ellipsis (if supported):**
```css
.text {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
```
## Designer Issues
### Hot reload not working
**Solutions:**
1. **Save the file** - Changes only reload on save
2. **Check file is in watch path**
3. **Restart designer** - Sometimes needed after many changes
4. **Check for syntax errors** - Invalid files may not reload
### Designer crashes on startup
**Solutions:**
1. **Check file paths:**
```bash
# Make sure path exists
mosis-designer.exe ../assets/main.rml
```
2. **Try a simple file first:**
```xml
<rml>
<head><title>Test</title></head>
<body><p>Hello</p></body>
</rml>
```
3. **Check for missing assets:**
Images or fonts that don't exist can cause crashes.
4. **Update graphics drivers:**
The designer uses OpenGL which requires up-to-date drivers.
### Rendering looks different on device
**Common causes:**
1. **Font differences** - Ensure fonts are bundled
2. **DPI scaling** - Use dp units consistently
3. **Color profiles** - Use standard sRGB colors
## Submission Issues
### "Version code must be higher"
Each new version needs a higher version_code.
**Solution:**
```json
{
"version": "1.0.1",
"version_code": 2 // Increment from previous
}
```
### "Signature verification failed"
Your package signature is invalid.
**Solutions:**
1. **Rebuild the package:**
```bash
mosis build
```
2. **Check signing key is registered:**
```bash
mosis keys list
```
3. **Re-register your key if needed:**
```bash
mosis keys register ~/.mosis/keys/production.pub
```
### "Review rejected"
Check the rejection reason in your dashboard. Common issues:
| Reason | Solution |
|--------|----------|
| Inappropriate content | Remove violating content |
| Misleading description | Update description to match functionality |
| Crashes on launch | Fix startup errors |
| Missing privacy policy | Add privacy policy for data-collecting apps |
| Impersonation | Don't copy other apps |
## Getting More Help
### Check Logs
**Designer logs:**
```bash
mosis-designer.exe app.rml --log debug.log
```
**Lua errors:**
```lua
-- Add error handling
local success, err = pcall(function()
-- Your code
end)
if not success then
print("Error:", err)
end
```
### Search Issues
Check if others have encountered the same issue:
- Developer forum
- GitHub issues
- Stack Overflow (tag: mosis)
### Contact Support
If you're still stuck:
1. Gather error messages and logs
2. Create minimal reproduction case
3. Submit through developer portal support
## See Also
- [FAQ](faq.md) - Frequently asked questions
- [Lua API Reference](api/lua-api.md) - API documentation
- [UI Design Guide](guides/ui-design.md) - Styling reference