add documentation site with markdown rendering (M12)
This commit is contained in:
535
portal/internal/web/docs/guides/best-practices.md
Normal file
535
portal/internal/web/docs/guides/best-practices.md
Normal 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("&", "&")
|
||||
text = text:gsub("<", "<")
|
||||
text = text:gsub(">", ">")
|
||||
|
||||
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
|
||||
506
portal/internal/web/docs/guides/lua-scripting.md
Normal file
506
portal/internal/web/docs/guides/lua-scripting.md
Normal 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
|
||||
396
portal/internal/web/docs/guides/permissions.md
Normal file
396
portal/internal/web/docs/guides/permissions.md
Normal 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
|
||||
395
portal/internal/web/docs/guides/ui-design.md
Normal file
395
portal/internal/web/docs/guides/ui-design.md
Normal 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
|
||||
Reference in New Issue
Block a user