{
    "type": "page",
    "title": "Welcome to Steedos",
    "body": [
        {
            "type": "liquid",
            "template": "<style>\n    .steedos-launcher {\n        font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', sans-serif;\n        -webkit-font-smoothing: antialiased;\n        -webkit-user-select: none;\n        user-select: none;\n    }\n\n    .no-scrollbar::-webkit-scrollbar { display: none; }\n    .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }\n\n    .app-icon-box {\n        transition: transform 0.18s ease, box-shadow 0.18s ease;\n        will-change: transform;\n        cursor: pointer;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n    }\n\n    .app-item:not(.static-item):hover .app-icon-box {\n        transform: scale(1.06);\n        box-shadow: 0 8px 24px -4px rgba(0,0,0,0.12);\n    }\n\n    .app-item:not(.static-item):active .app-icon-box {\n        transform: scale(0.97);\n        box-shadow: 0 2px 8px rgba(0,0,0,0.08);\n    }\n\n    .app-item.static-item:hover .app-icon-box {\n        background-color: rgba(0,0,0,0.04);\n    }\n\n    .sortable-drag .app-icon-box {\n        transform: scale(1.1) !important;\n        box-shadow: 0 20px 40px -8px rgba(0,0,0,0.18);\n        cursor: grabbing;\n    }\n\n    .sortable-ghost { opacity: 0; }\n\n    .loading-skeleton {\n        animation: sk-pulse 1.6s ease-in-out infinite;\n        background-color: rgba(0,0,0,0.06);\n        border-radius: 1.25rem;\n    }\n\n    @keyframes sk-pulse {\n        0%, 100% { opacity: 1; }\n        50% { opacity: 0.4; }\n    }\n</style>\n\n<div id=\"launcher-container\" class=\"steedos-launcher relative w-full text-neutral-800\">\n    <div class=\"relative z-10 w-full flex flex-col items-center pt-6 pb-4 px-4 no-scrollbar\" id=\"main-scroll-area\">\n        <div id=\"vibrant-grid\"\n            class=\"bg-white rounded-xl shadow-sm grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8 gap-x-5 gap-y-6 w-full max-w-4xl mx-auto py-6 px-8\">\n        </div>\n    </div>\n</div>\n\n<script>\n    window.navigateTo = function(path) {\n        if (!path) return;\n        if (path.startsWith('/') || path.startsWith(window.location.origin)) {\n            try {\n                var url = new URL(path, window.location.origin);\n                window.navigate(url.pathname + url.search + url.hash);\n            } catch(e) {\n                window.navigate(path);\n            }\n        } else {\n            window.location.href = path;\n        }\n    };\n\n    window.handleAppStoreClick = function() {\n        window.navigateTo(\"/app/admin/page/app_store\");\n    };\n\n    window.handleAddAppClick = function() {\n        data._scoped.doAction([{\n            actionType: 'click',\n            componentId: 'buttonAddApp',\n        }])\n    };\n\n    var STORE_KEY = 'steedos_launcher_order';\n    var COLOR_MAP = {\n        'red':    'bg-red-500',\n        'orange': 'bg-orange-500',\n        'yellow': 'bg-amber-500',\n        'green':  'bg-emerald-500',\n        'teal':   'bg-teal-500',\n        'cyan':   'bg-cyan-500',\n        'sky':    'bg-sky-500',\n        'blue':   'bg-blue-500',\n        'purple': 'bg-violet-500',\n        'pink':   'bg-pink-500',\n        'gray':   'bg-neutral-500',\n        'default':'bg-sky-500'\n    };\n\n    function saveAppOrder(orderArray) {\n        var cleanOrder = orderArray.filter(function(id) { return id !== 'add'; });\n        localStorage.setItem(STORE_KEY, JSON.stringify(cleanOrder));\n    }\n\n    function createAppHtml(app) {\n        var colorKey = app.color;\n        if (!colorKey) colorKey = 'sky';\n        if (app.id === 'admin' && !app.color) colorKey = 'gray';\n        var bgClass = COLOR_MAP[colorKey] || COLOR_MAP['sky'];\n\n        var iconName = app.icon || 'account';\n        var iconPath = '/assets/icons/standard-sprite/svg/symbols.svg#' + iconName;\n\n        return `\n            <div class=\"app-item group flex flex-col items-center select-none\"\n                 data-id=\"\\${app.id}\"\n                 onclick=\"window.navigateTo('\\${app.path}')\">\n                <div class=\"app-icon-box relative w-16 h-16 md:w-[72px] md:h-[72px] rounded-2xl \\${bgClass} text-white shadow-sm\">\n                    <svg class=\"w-9 h-9 md:w-10 md:h-10\" aria-hidden=\"true\" fill=\"currentColor\">\n                        <use xlink:href=\"\\${iconPath}\"></use>\n                    </svg>\n                </div>\n                <span class=\"mt-2 text-[11px] md:text-xs text-neutral-500 group-hover:text-neutral-800 transition-colors text-center leading-tight line-clamp-2 w-16 md:w-[72px]\">\\${app.name}</span>\n            </div>\n        `;\n    }\n\n    function createAddBtnHtml() {\n        return `\n            <div class=\"app-item group flex flex-col items-center select-none static-item\" data-id=\"add\" onclick=\"handleAddAppClick()\">\n                <div class=\"app-icon-box relative w-16 h-16 md:w-[72px] md:h-[72px] rounded-2xl border border-dashed border-neutral-300 text-neutral-300\">\n                    <svg class=\"w-8 h-8 md:w-9 md:h-9\" aria-hidden=\"true\" fill=\"currentColor\">\n                        <use xlink:href=\"/assets/icons/utility-sprite/svg/symbols.svg#add\"></use>\n                    </svg>\n                </div>\n                <span class=\"mt-2 text-[11px] md:text-xs text-neutral-400\">添加应用</span>\n            </div>\n        `;\n    }\n\n    (async function initLauncher() {\n        var gridElement = document.getElementById('vibrant-grid');\n        var skeletonHtml = '';\n        for(var i=0; i<8; i++) { skeletonHtml += '<div class=\"w-16 h-16 md:w-[72px] md:h-[72px] loading-skeleton mx-auto\"></div>'; }\n        gridElement.innerHTML = skeletonHtml;\n\n        try {\n            var isMobile = window.innerWidth < 768;\n            var response = await fetch('/service/api/apps/menus?mobile=' + isMobile);\n            if (!response.ok) throw new Error('Failed to fetch apps');\n            var appsData = (await response.json()).filter(function(a) { return a.id !== 'desktop'; });\n\n            var savedOrderStr = localStorage.getItem(STORE_KEY);\n            var finalApps = [];\n\n            if (savedOrderStr) {\n                var savedOrderIds = JSON.parse(savedOrderStr);\n                savedOrderIds.forEach(function(id) {\n                    var app = appsData.find(function(a) { return a.id === id; });\n                    if (app) finalApps.push(app);\n                });\n                appsData.forEach(function(app) {\n                    if (savedOrderIds.indexOf(app.id) === -1) finalApps.push(app);\n                });\n            } else {\n                finalApps = appsData;\n            }\n\n            var htmlContent = finalApps.map(function(app) { return createAppHtml(app); }).join('');\n            htmlContent += createAddBtnHtml();\n            gridElement.innerHTML = htmlContent;\n\n            if (typeof Sortable !== 'undefined') {\n                new Sortable(gridElement, {\n                    animation: 300,\n                    easing: \"ease\",\n                    delay: 0,\n                    delayOnTouchOnly: true,\n                    ghostClass: 'sortable-ghost',\n                    dragClass: 'sortable-drag',\n                    filter: \".static-item\",\n                    onMove: function (evt) { return evt.related.className.indexOf('static-item') === -1; },\n                    onEnd: function (evt) {\n                        var newOrder = [];\n                        var currentItems = gridElement.querySelectorAll('.app-item');\n                        currentItems.forEach(function(item) {\n                            var id = item.getAttribute('data-id');\n                            if (id && id !== 'add') newOrder.push(id);\n                        });\n                        saveAppOrder(newOrder);\n                    }\n                });\n            }\n        } catch (error) {\n            console.error(\"Launcher Error:\", error);\n            gridElement.innerHTML = '<div class=\"col-span-full text-center text-neutral-400 py-20\">加载失败<br><span class=\"text-xs opacity-60\">' + error.message + '</span></div>';\n        }\n    })();\n</script>",
            "id": "u:04103f69c7d4"
        },
        {
            "type": "service",
            "schemaApi": "get:/service/api/@steedos/service-pages/page/app_launcher/widget_schema",
            "id": "u:app_launcher_widgets",
            "className": "w-full max-w-4xl mx-auto p-0 m-0"
        },
        {
            "type": "button",
            "label": "按钮",
            "onEvent": {
                "click": {
                    "actions": [
                        {
                            "ignoreError": false,
                            "actionType": "dialog",
                            "dialog": {
                                "type": "dialog",
                                "title": "新建应用",
                                "body": [
                                    {
                                        "type": "steedos-object-form",
                                        "label": "对象表单",
                                        "objectApiName": "apps",
                                        "recordId": "${recordId}",
                                        "mode": "edit",
                                        "enableInitApi": false,
                                        "className": "",
                                        "id": "u:b81c61018eeb",
                                        "fields": [
                                            "name",
                                            "code",
                                            "color",
                                            "icon_slds",
                                            "dark"
                                        ],
                                        "enableTabs": false,
                                        "tabsMode": "",
                                        "onSubmitSuccess": "document.location.reload();"
                                    }
                                ],
                                "id": "u:6afe7f1c03a3",
                                "actions": [
                                    {
                                        "type": "button",
                                        "actionType": "cancel",
                                        "label": "取消",
                                        "id": "u:7c749cf8acad"
                                    },
                                    {
                                        "type": "button",
                                        "actionType": "confirm",
                                        "label": "确定",
                                        "primary": true,
                                        "id": "u:6ba48c5527ec"
                                    }
                                ],
                                "showCloseButton": true,
                                "closeOnOutside": false,
                                "closeOnEsc": false,
                                "showErrorMsg": true,
                                "showLoading": true,
                                "draggable": false,
                                "actionType": "dialog",
                                "size": "md"
                            }
                        }
                    ]
                }
            },
            "id": "buttonAddApp",
            "editorState": "default",
            "visible": true,
            "hidden": false,
            "wrapperCustomStyle": {},
            "className": "hidden"
        }
    ],
    "regions": [
        "body"
    ],
    "data": {
        "initialValues": {},
        "appId": "builder",
        "title": ""
    },
    "id": "u:d3e47da07a71",
    "asideResizor": false,
    "editorState": "default",
    "pullRefresh": {
        "disabled": true
    },
    "themeCss": {
        "bodyControlClassName": {
            "padding-and-margin:default": {
                "paddingTop": "0",
                "paddingRight": "0",
                "paddingBottom": "0",
                "paddingLeft": "0"
            }
        }
    },
    "definitions": {}
}