sh3-core 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/MenuButton.svelte +2 -1
- package/dist/actions/contextMenuModel.d.ts +1 -1
- package/dist/actions/contextMenuModel.js +2 -1
- package/dist/actions/dispatcher.svelte.d.ts +1 -1
- package/dist/actions/dispatcher.svelte.js +2 -1
- package/dist/actions/listActive.d.ts +1 -1
- package/dist/actions/listActive.js +2 -1
- package/dist/actions/listeners.d.ts +1 -1
- package/dist/actions/listeners.js +6 -5
- package/dist/actions/menuBarModel.js +3 -2
- package/dist/actions/paletteModel.js +2 -1
- package/dist/actions/resolveLabel.test.d.ts +1 -0
- package/dist/actions/resolveLabel.test.js +14 -0
- package/dist/actions/types.d.ts +12 -1
- package/dist/actions/types.js +7 -1
- package/dist/app/store/AppUpdateAvailableModal.svelte +87 -0
- package/dist/app/store/AppUpdateAvailableModal.svelte.d.ts +11 -0
- package/dist/app/store/InstalledView.svelte +8 -54
- package/dist/app/store/UninstallAppDialog.svelte +86 -0
- package/dist/app/store/UninstallAppDialog.svelte.d.ts +10 -0
- package/dist/app/store/permissionConfirm.d.ts +4 -0
- package/dist/app/store/permissionConfirm.js +28 -0
- package/dist/app/store/storeShard.svelte.d.ts +8 -1
- package/dist/app/store/storeShard.svelte.js +42 -9
- package/dist/app/store/updatePackage.test.d.ts +1 -0
- package/dist/app/store/updatePackage.test.js +34 -0
- package/dist/app/store/verbs.d.ts +1 -0
- package/dist/app/store/verbs.js +79 -5
- package/dist/app/store/verbs.test.d.ts +1 -0
- package/dist/app/store/verbs.test.js +56 -0
- package/dist/app-appearance/AppAppearanceModal.svelte +174 -0
- package/dist/app-appearance/AppAppearanceModal.svelte.d.ts +8 -0
- package/dist/app-appearance/appearanceShard.svelte.d.ts +2 -0
- package/dist/app-appearance/appearanceShard.svelte.js +61 -0
- package/dist/app-appearance/appearanceState.svelte.d.ts +15 -0
- package/dist/app-appearance/appearanceState.svelte.js +59 -0
- package/dist/app-appearance/appearanceState.test.d.ts +1 -0
- package/dist/app-appearance/appearanceState.test.js +30 -0
- package/dist/app-appearance/index.d.ts +3 -0
- package/dist/app-appearance/index.js +2 -0
- package/dist/app-appearance/types.d.ts +11 -0
- package/dist/app-appearance/types.js +1 -0
- package/dist/apps/types.d.ts +7 -0
- package/dist/assets/iconIds.generated.d.ts +2 -0
- package/dist/assets/iconIds.generated.js +154 -0
- package/dist/host.js +2 -1
- package/dist/primitives/widgets/IconPicker.svelte +115 -0
- package/dist/primitives/widgets/IconPicker.svelte.d.ts +9 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.d.ts +1 -0
- package/dist/primitives/widgets/IconPicker.svelte.test.js +43 -0
- package/dist/projects-shard/ProjectManage.svelte +14 -4
- package/dist/sh3core-shard/ShellHome.svelte +64 -38
- package/dist/sh3core-shard/appActions.d.ts +13 -0
- package/dist/sh3core-shard/appActions.js +181 -0
- package/dist/sh3core-shard/appActions.test.d.ts +1 -0
- package/dist/sh3core-shard/appActions.test.js +25 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { setAppearance, getAppearance, __resetForTests } from './appearanceState.svelte';
|
|
3
|
+
describe('appearanceShard get/set', () => {
|
|
4
|
+
beforeEach(() => __resetForTests());
|
|
5
|
+
it('returns undefined for an unset app', () => {
|
|
6
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
7
|
+
});
|
|
8
|
+
it('round-trips an icon override', () => {
|
|
9
|
+
setAppearance('foo', { icon: 'house' });
|
|
10
|
+
expect(getAppearance('foo')).toEqual({ icon: 'house' });
|
|
11
|
+
});
|
|
12
|
+
it('round-trips a color override', () => {
|
|
13
|
+
setAppearance('foo', { color: '#ff0000' });
|
|
14
|
+
expect(getAppearance('foo')).toEqual({ color: '#ff0000' });
|
|
15
|
+
});
|
|
16
|
+
it('round-trips a label override', () => {
|
|
17
|
+
setAppearance('foo', { label: 'My Label' });
|
|
18
|
+
expect(getAppearance('foo')).toEqual({ label: 'My Label' });
|
|
19
|
+
});
|
|
20
|
+
it('clears the entry when all fields are undefined', () => {
|
|
21
|
+
setAppearance('foo', { icon: 'house', color: '#ff0000', label: 'X' });
|
|
22
|
+
setAppearance('foo', { icon: undefined, color: undefined, label: undefined });
|
|
23
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('clears the entry when given undefined directly', () => {
|
|
26
|
+
setAppearance('foo', { icon: 'house' });
|
|
27
|
+
setAppearance('foo', undefined);
|
|
28
|
+
expect(getAppearance('foo')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-app per-user-per-browser visual override. Stored in the user zone
|
|
3
|
+
* of the __app-appearance__ shard. Every field is optional — an empty
|
|
4
|
+
* AppAppearance object is treated as "no override" and removed from the
|
|
5
|
+
* map by setAppearance().
|
|
6
|
+
*/
|
|
7
|
+
export interface AppAppearance {
|
|
8
|
+
icon?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/apps/types.d.ts
CHANGED
|
@@ -74,6 +74,13 @@ export interface AppManifest {
|
|
|
74
74
|
* When absent, the canonical fallback is used. See MenuContainer.
|
|
75
75
|
*/
|
|
76
76
|
menus?: MenuContainer[];
|
|
77
|
+
/**
|
|
78
|
+
* Optional default home-card icon — a lucide icon id from the bundled
|
|
79
|
+
* sprite (see `src/assets/icons.svg`). User-set per-browser overrides
|
|
80
|
+
* (via the home-card Customize action) take precedence; if neither is
|
|
81
|
+
* set the framework falls back to `box`.
|
|
82
|
+
*/
|
|
83
|
+
icon?: string;
|
|
77
84
|
}
|
|
78
85
|
/**
|
|
79
86
|
* Context object passed to `App.activate`. Provides app-scoped state zones
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const ICON_IDS: readonly ["activity", "align-horizontal-justify-center", "align-horizontal-justify-end", "align-horizontal-justify-start", "app-window", "archive", "archive-restore", "axis-3d", "box", "brick-wall", "bug", "building-2", "cable", "calendar", "camera", "check", "chevron-down", "chevron-right", "circle-check", "circle-dot", "circle-minus", "circle-x", "clipboard", "clipboard-paste", "clock", "compass", "component", "copy", "cpu", "crop", "crosshair", "crown", "dollar-sign", "download", "droplet", "eraser", "euro", "external-link", "eye", "eye-off", "file", "file-archive", "file-diff", "file-plus", "file-text", "flame", "flip-horizontal-2", "flip-vertical-2", "folder", "folder-open", "folder-plus", "folder-tree", "gallery-vertical-end", "gamepad-2", "gauge", "gem", "git-branch", "git-commit-horizontal", "git-merge", "globe", "grid-2x2", "grid-3x3", "group", "hard-drive", "heart", "history", "house", "image", "info", "joystick", "key", "layers", "layout-dashboard", "layout-grid", "layout-list", "layout-panel-left", "layout-panel-top", "layout-template", "lightbulb", "link", "list-ordered", "list-tree", "lock", "log-out", "magnet", "mail", "map", "maximize", "minimize", "moon", "mouse-pointer", "move", "move-3d", "music", "navigation", "network", "notebook-pen", "palette", "pause", "pencil", "pipette", "play", "plus", "pointer", "pound-sterling", "receipt", "redo-2", "refresh-cw", "rocket", "rotate-3d", "rotate-ccw", "rotate-cw", "ruler", "save", "scissors", "scroll-text", "search", "send", "server", "settings", "shield", "skull", "sliders-horizontal", "snowflake", "sparkles", "square", "square-terminal", "star", "sun", "sword", "table-properties", "target", "texture", "timer", "trash-2", "triangle-alert", "type", "undo-2", "ungroup", "unity", "upload", "user", "users", "video", "volume-2", "wand-sparkles", "wind", "x", "zap", "zoom-in", "zoom-out"];
|
|
2
|
+
export type IconId = (typeof ICON_IDS)[number];
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// GENERATED — do not edit. See scripts/sync-icon-ids.ts
|
|
2
|
+
export const ICON_IDS = [
|
|
3
|
+
'activity',
|
|
4
|
+
'align-horizontal-justify-center',
|
|
5
|
+
'align-horizontal-justify-end',
|
|
6
|
+
'align-horizontal-justify-start',
|
|
7
|
+
'app-window',
|
|
8
|
+
'archive',
|
|
9
|
+
'archive-restore',
|
|
10
|
+
'axis-3d',
|
|
11
|
+
'box',
|
|
12
|
+
'brick-wall',
|
|
13
|
+
'bug',
|
|
14
|
+
'building-2',
|
|
15
|
+
'cable',
|
|
16
|
+
'calendar',
|
|
17
|
+
'camera',
|
|
18
|
+
'check',
|
|
19
|
+
'chevron-down',
|
|
20
|
+
'chevron-right',
|
|
21
|
+
'circle-check',
|
|
22
|
+
'circle-dot',
|
|
23
|
+
'circle-minus',
|
|
24
|
+
'circle-x',
|
|
25
|
+
'clipboard',
|
|
26
|
+
'clipboard-paste',
|
|
27
|
+
'clock',
|
|
28
|
+
'compass',
|
|
29
|
+
'component',
|
|
30
|
+
'copy',
|
|
31
|
+
'cpu',
|
|
32
|
+
'crop',
|
|
33
|
+
'crosshair',
|
|
34
|
+
'crown',
|
|
35
|
+
'dollar-sign',
|
|
36
|
+
'download',
|
|
37
|
+
'droplet',
|
|
38
|
+
'eraser',
|
|
39
|
+
'euro',
|
|
40
|
+
'external-link',
|
|
41
|
+
'eye',
|
|
42
|
+
'eye-off',
|
|
43
|
+
'file',
|
|
44
|
+
'file-archive',
|
|
45
|
+
'file-diff',
|
|
46
|
+
'file-plus',
|
|
47
|
+
'file-text',
|
|
48
|
+
'flame',
|
|
49
|
+
'flip-horizontal-2',
|
|
50
|
+
'flip-vertical-2',
|
|
51
|
+
'folder',
|
|
52
|
+
'folder-open',
|
|
53
|
+
'folder-plus',
|
|
54
|
+
'folder-tree',
|
|
55
|
+
'gallery-vertical-end',
|
|
56
|
+
'gamepad-2',
|
|
57
|
+
'gauge',
|
|
58
|
+
'gem',
|
|
59
|
+
'git-branch',
|
|
60
|
+
'git-commit-horizontal',
|
|
61
|
+
'git-merge',
|
|
62
|
+
'globe',
|
|
63
|
+
'grid-2x2',
|
|
64
|
+
'grid-3x3',
|
|
65
|
+
'group',
|
|
66
|
+
'hard-drive',
|
|
67
|
+
'heart',
|
|
68
|
+
'history',
|
|
69
|
+
'house',
|
|
70
|
+
'image',
|
|
71
|
+
'info',
|
|
72
|
+
'joystick',
|
|
73
|
+
'key',
|
|
74
|
+
'layers',
|
|
75
|
+
'layout-dashboard',
|
|
76
|
+
'layout-grid',
|
|
77
|
+
'layout-list',
|
|
78
|
+
'layout-panel-left',
|
|
79
|
+
'layout-panel-top',
|
|
80
|
+
'layout-template',
|
|
81
|
+
'lightbulb',
|
|
82
|
+
'link',
|
|
83
|
+
'list-ordered',
|
|
84
|
+
'list-tree',
|
|
85
|
+
'lock',
|
|
86
|
+
'log-out',
|
|
87
|
+
'magnet',
|
|
88
|
+
'mail',
|
|
89
|
+
'map',
|
|
90
|
+
'maximize',
|
|
91
|
+
'minimize',
|
|
92
|
+
'moon',
|
|
93
|
+
'mouse-pointer',
|
|
94
|
+
'move',
|
|
95
|
+
'move-3d',
|
|
96
|
+
'music',
|
|
97
|
+
'navigation',
|
|
98
|
+
'network',
|
|
99
|
+
'notebook-pen',
|
|
100
|
+
'palette',
|
|
101
|
+
'pause',
|
|
102
|
+
'pencil',
|
|
103
|
+
'pipette',
|
|
104
|
+
'play',
|
|
105
|
+
'plus',
|
|
106
|
+
'pointer',
|
|
107
|
+
'pound-sterling',
|
|
108
|
+
'receipt',
|
|
109
|
+
'redo-2',
|
|
110
|
+
'refresh-cw',
|
|
111
|
+
'rocket',
|
|
112
|
+
'rotate-3d',
|
|
113
|
+
'rotate-ccw',
|
|
114
|
+
'rotate-cw',
|
|
115
|
+
'ruler',
|
|
116
|
+
'save',
|
|
117
|
+
'scissors',
|
|
118
|
+
'scroll-text',
|
|
119
|
+
'search',
|
|
120
|
+
'send',
|
|
121
|
+
'server',
|
|
122
|
+
'settings',
|
|
123
|
+
'shield',
|
|
124
|
+
'skull',
|
|
125
|
+
'sliders-horizontal',
|
|
126
|
+
'snowflake',
|
|
127
|
+
'sparkles',
|
|
128
|
+
'square',
|
|
129
|
+
'square-terminal',
|
|
130
|
+
'star',
|
|
131
|
+
'sun',
|
|
132
|
+
'sword',
|
|
133
|
+
'table-properties',
|
|
134
|
+
'target',
|
|
135
|
+
'texture',
|
|
136
|
+
'timer',
|
|
137
|
+
'trash-2',
|
|
138
|
+
'triangle-alert',
|
|
139
|
+
'type',
|
|
140
|
+
'undo-2',
|
|
141
|
+
'ungroup',
|
|
142
|
+
'unity',
|
|
143
|
+
'upload',
|
|
144
|
+
'user',
|
|
145
|
+
'users',
|
|
146
|
+
'video',
|
|
147
|
+
'volume-2',
|
|
148
|
+
'wand-sparkles',
|
|
149
|
+
'wind',
|
|
150
|
+
'x',
|
|
151
|
+
'zap',
|
|
152
|
+
'zoom-in',
|
|
153
|
+
'zoom-out',
|
|
154
|
+
];
|
package/dist/host.js
CHANGED
|
@@ -23,6 +23,7 @@ import { sh3coreShard } from './sh3core-shard/sh3coreShard.svelte';
|
|
|
23
23
|
import { shellShard } from './shell-shard/shellShard.svelte';
|
|
24
24
|
import { storeShard } from './app/store/storeShard.svelte';
|
|
25
25
|
import { projectsShard } from './projects-shard/projectsShard.svelte';
|
|
26
|
+
import { appearanceShard } from './app-appearance';
|
|
26
27
|
import { __setBackend, backends } from './state/zones.svelte';
|
|
27
28
|
import { loadInstalledPackages } from './registry/installer';
|
|
28
29
|
import { setLocalOwner } from './auth/index';
|
|
@@ -67,7 +68,7 @@ export async function bootstrap(config) {
|
|
|
67
68
|
}
|
|
68
69
|
const exShards = new Set(config === null || config === void 0 ? void 0 : config.excludeShards);
|
|
69
70
|
// 1. Framework-owned shards
|
|
70
|
-
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard];
|
|
71
|
+
const frameworkShards = [sh3coreShard, shellShard, storeShard, adminShard, projectsShard, appearanceShard];
|
|
71
72
|
for (const shard of frameworkShards) {
|
|
72
73
|
if (!exShards.has(shard.manifest.id)) {
|
|
73
74
|
registerShardInternal(shard);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/*
|
|
3
|
+
* IconPicker — searchable grid of icon ids from the bundled lucide
|
|
4
|
+
* sprite. The "(none)" tile clears the value. Composes the widget
|
|
5
|
+
* commit-only event contract (CommitOnlyEvents<string | undefined>).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CommitOnlyEvents } from './_contract';
|
|
9
|
+
import { ICON_IDS } from '../../assets/iconIds.generated';
|
|
10
|
+
import iconsUrl from '../../assets/icons.svg';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
value?: string | undefined;
|
|
14
|
+
label?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
} & CommitOnlyEvents<string | undefined>;
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
value = $bindable(undefined),
|
|
20
|
+
label,
|
|
21
|
+
disabled = false,
|
|
22
|
+
onchange,
|
|
23
|
+
}: Props = $props();
|
|
24
|
+
|
|
25
|
+
let query = $state('');
|
|
26
|
+
|
|
27
|
+
const filtered = $derived(
|
|
28
|
+
query.trim() === ''
|
|
29
|
+
? ICON_IDS
|
|
30
|
+
: ICON_IDS.filter((id) => id.includes(query.trim().toLowerCase())),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
function pick(id: string | undefined): void {
|
|
34
|
+
if (disabled) return;
|
|
35
|
+
value = id;
|
|
36
|
+
onchange?.(id);
|
|
37
|
+
}
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<div class="sh3-icon-picker" class:sh3-icon-picker--disabled={disabled}>
|
|
41
|
+
{#if label}<span class="sh3-icon-picker__label">{label}</span>{/if}
|
|
42
|
+
<input
|
|
43
|
+
type="search"
|
|
44
|
+
placeholder="Search icons…"
|
|
45
|
+
bind:value={query}
|
|
46
|
+
{disabled}
|
|
47
|
+
aria-label="Filter icons"
|
|
48
|
+
/>
|
|
49
|
+
<div class="sh3-icon-picker__grid">
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="sh3-icon-picker__tile sh3-icon-picker__tile--none"
|
|
53
|
+
class:sh3-icon-picker__tile--selected={value === undefined}
|
|
54
|
+
aria-label="No icon"
|
|
55
|
+
{disabled}
|
|
56
|
+
onclick={() => pick(undefined)}
|
|
57
|
+
>×</button>
|
|
58
|
+
{#each filtered as id (id)}
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
class="sh3-icon-picker__tile"
|
|
62
|
+
class:sh3-icon-picker__tile--selected={value === id}
|
|
63
|
+
aria-label={id}
|
|
64
|
+
title={id}
|
|
65
|
+
{disabled}
|
|
66
|
+
onclick={() => pick(id)}
|
|
67
|
+
>
|
|
68
|
+
<svg viewBox="0 0 24 24"><use href="{iconsUrl}#{id}" /></svg>
|
|
69
|
+
</button>
|
|
70
|
+
{/each}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<style>
|
|
75
|
+
.sh3-icon-picker { display: flex; flex-direction: column; gap: 6px; }
|
|
76
|
+
.sh3-icon-picker__label { font-size: 13px; color: var(--shell-fg-muted); }
|
|
77
|
+
.sh3-icon-picker input[type="search"] {
|
|
78
|
+
background: var(--shell-bg-elevated);
|
|
79
|
+
color: var(--shell-fg);
|
|
80
|
+
border: 1px solid var(--shell-border);
|
|
81
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
82
|
+
padding: 6px 8px; font: inherit; font-size: 13px;
|
|
83
|
+
}
|
|
84
|
+
.sh3-icon-picker__grid {
|
|
85
|
+
display: grid;
|
|
86
|
+
grid-template-columns: repeat(auto-fill, minmax(36px, 1fr));
|
|
87
|
+
gap: 4px;
|
|
88
|
+
max-height: 220px;
|
|
89
|
+
overflow-y: auto;
|
|
90
|
+
padding: 4px;
|
|
91
|
+
background: var(--shell-bg);
|
|
92
|
+
border: 1px solid var(--shell-border);
|
|
93
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
94
|
+
}
|
|
95
|
+
.sh3-icon-picker__tile {
|
|
96
|
+
aspect-ratio: 1 / 1;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
background: var(--shell-bg-elevated);
|
|
101
|
+
color: var(--shell-fg);
|
|
102
|
+
border: 1px solid transparent;
|
|
103
|
+
border-radius: var(--shell-radius-sm, 3px);
|
|
104
|
+
cursor: pointer;
|
|
105
|
+
padding: 0; font: inherit;
|
|
106
|
+
}
|
|
107
|
+
.sh3-icon-picker__tile--none { font-size: 16px; color: var(--shell-fg-muted); }
|
|
108
|
+
.sh3-icon-picker__tile:hover { border-color: var(--shell-accent); }
|
|
109
|
+
.sh3-icon-picker__tile--selected {
|
|
110
|
+
outline: 2px solid var(--shell-accent);
|
|
111
|
+
outline-offset: -2px;
|
|
112
|
+
}
|
|
113
|
+
.sh3-icon-picker__tile svg { width: 18px; height: 18px; }
|
|
114
|
+
.sh3-icon-picker--disabled .sh3-icon-picker__tile { cursor: not-allowed; opacity: 0.5; }
|
|
115
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CommitOnlyEvents } from './_contract';
|
|
2
|
+
type Props = {
|
|
3
|
+
value?: string | undefined;
|
|
4
|
+
label?: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
} & CommitOnlyEvents<string | undefined>;
|
|
7
|
+
declare const IconPicker: import("svelte").Component<Props, {}, "value">;
|
|
8
|
+
type IconPicker = ReturnType<typeof IconPicker>;
|
|
9
|
+
export default IconPicker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { render, fireEvent } from '@testing-library/svelte';
|
|
3
|
+
import IconPicker from './IconPicker.svelte';
|
|
4
|
+
describe('IconPicker', () => {
|
|
5
|
+
it('renders the (none) tile and at least one icon tile', () => {
|
|
6
|
+
const { container } = render(IconPicker, { props: {} });
|
|
7
|
+
const tiles = container.querySelectorAll('.sh3-icon-picker__tile');
|
|
8
|
+
expect(tiles.length).toBeGreaterThan(1);
|
|
9
|
+
expect(tiles[0].getAttribute('aria-label')).toBe('No icon');
|
|
10
|
+
});
|
|
11
|
+
it('filters tiles by search input', async () => {
|
|
12
|
+
const { container } = render(IconPicker, { props: {} });
|
|
13
|
+
const search = container.querySelector('input[type="search"]');
|
|
14
|
+
await fireEvent.input(search, { target: { value: 'house' } });
|
|
15
|
+
const visible = container.querySelectorAll('.sh3-icon-picker__tile:not([hidden])');
|
|
16
|
+
expect(visible.length).toBeGreaterThan(0);
|
|
17
|
+
expect(visible.length).toBeLessThan(20);
|
|
18
|
+
});
|
|
19
|
+
it('calls onchange with the clicked icon id', async () => {
|
|
20
|
+
let lastValue = '__sentinel__';
|
|
21
|
+
const { container } = render(IconPicker, {
|
|
22
|
+
props: {
|
|
23
|
+
onchange: (v) => { lastValue = v; },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
const tiles = Array.from(container.querySelectorAll('.sh3-icon-picker__tile'));
|
|
27
|
+
await fireEvent.click(tiles[1]);
|
|
28
|
+
expect(lastValue).toEqual(expect.any(String));
|
|
29
|
+
expect(lastValue).not.toBe('__sentinel__');
|
|
30
|
+
});
|
|
31
|
+
it('calls onchange with undefined when (none) tile is clicked', async () => {
|
|
32
|
+
let lastValue = 'house';
|
|
33
|
+
const { container } = render(IconPicker, {
|
|
34
|
+
props: {
|
|
35
|
+
value: 'house',
|
|
36
|
+
onchange: (v) => { lastValue = v; },
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
const noneTile = container.querySelector('.sh3-icon-picker__tile');
|
|
40
|
+
await fireEvent.click(noneTile);
|
|
41
|
+
expect(lastValue).toBeUndefined();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* "include myself" works without looking up the underlying user id.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { untrack } from 'svelte';
|
|
11
12
|
import { projectsApi, type ProjectRecord } from './projectsApi';
|
|
12
13
|
import { refreshProjects } from './projectsShard.svelte';
|
|
13
14
|
import { getUser } from '../auth/auth.svelte';
|
|
@@ -26,12 +27,21 @@
|
|
|
26
27
|
const isEdit = $derived(project !== null);
|
|
27
28
|
const me = $derived(getUser());
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// Form fields snapshot the project on mount; the modal is recreated per
|
|
31
|
+
// open, so prop changes after mount aren't expected. untrack defers the
|
|
32
|
+
// reactive read so Svelte 5 doesn't emit state_referenced_locally.
|
|
33
|
+
let name = $state(untrack(() => project?.name ?? ''));
|
|
34
|
+
let description = $state(untrack(() => project?.description ?? ''));
|
|
31
35
|
let members = $state<string[]>(
|
|
32
|
-
|
|
36
|
+
untrack(() => {
|
|
37
|
+
if (project) return [...project.members];
|
|
38
|
+
const userId = getUser()?.id;
|
|
39
|
+
return userId ? [userId] : [];
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
let appAllowlist = $state<string[]>(
|
|
43
|
+
untrack(() => (project ? [...project.appAllowlist] : [])),
|
|
33
44
|
);
|
|
34
|
-
let appAllowlist = $state<string[]>(project ? [...project.appAllowlist] : []);
|
|
35
45
|
let saving = $state(false);
|
|
36
46
|
let error = $state<string | null>(null);
|
|
37
47
|
|
|
@@ -12,6 +12,22 @@
|
|
|
12
12
|
import ProjectsSection from '../projects-shard/ProjectsSection.svelte';
|
|
13
13
|
import { sessionState } from '../projects/session-state.svelte';
|
|
14
14
|
import { projectsState } from '../projects-shard/projectsShard.svelte';
|
|
15
|
+
import { shell } from '../shellRuntime.svelte';
|
|
16
|
+
import { makeSelectionApi } from '../actions/selection.svelte';
|
|
17
|
+
import { getAppearance } from '../app-appearance';
|
|
18
|
+
import iconsUrl from '../assets/icons.svg';
|
|
19
|
+
|
|
20
|
+
const homeSelection = makeSelectionApi('__sh3core__');
|
|
21
|
+
|
|
22
|
+
function openAppContextMenu(event: MouseEvent, appId: string): void {
|
|
23
|
+
event.preventDefault();
|
|
24
|
+
homeSelection.set({ type: 'app', ref: { appId } });
|
|
25
|
+
shell.actions.openContextMenu({
|
|
26
|
+
x: event.clientX,
|
|
27
|
+
y: event.clientY,
|
|
28
|
+
scope: { element: 'app' },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
15
31
|
|
|
16
32
|
let filter = $state('');
|
|
17
33
|
|
|
@@ -71,17 +87,20 @@
|
|
|
71
87
|
<h2 class="shell-home-section-title">Apps</h2>
|
|
72
88
|
<div class="shell-home-grid">
|
|
73
89
|
{#each userApps as manifest (manifest.id)}
|
|
90
|
+
{@const appearance = getAppearance(manifest.id)}
|
|
74
91
|
<button
|
|
75
92
|
type="button"
|
|
76
93
|
class="shell-home-card"
|
|
94
|
+
class:shell-home-card--tinted={appearance?.color}
|
|
95
|
+
style:--card-color={appearance?.color ?? 'transparent'}
|
|
96
|
+
data-sh3-scope="element:app"
|
|
77
97
|
onclick={() => launchApp(manifest.id)}
|
|
78
|
-
|
|
98
|
+
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
79
99
|
>
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
</div>
|
|
100
|
+
<span class="shell-home-card-square">
|
|
101
|
+
<svg class="shell-home-card-icon"><use href="{iconsUrl}#{appearance?.icon ?? manifest.icon ?? 'box'}" /></svg>
|
|
102
|
+
</span>
|
|
103
|
+
<span class="shell-home-card-label">{appearance?.label ?? manifest.label}</span>
|
|
85
104
|
</button>
|
|
86
105
|
{/each}
|
|
87
106
|
</div>
|
|
@@ -93,17 +112,20 @@
|
|
|
93
112
|
<h2 class="shell-home-section-title">Admin</h2>
|
|
94
113
|
<div class="shell-home-grid">
|
|
95
114
|
{#each adminApps as manifest (manifest.id)}
|
|
115
|
+
{@const appearance = getAppearance(manifest.id)}
|
|
96
116
|
<button
|
|
97
117
|
type="button"
|
|
98
118
|
class="shell-home-card"
|
|
119
|
+
class:shell-home-card--tinted={appearance?.color}
|
|
120
|
+
style:--card-color={appearance?.color ?? 'transparent'}
|
|
121
|
+
data-sh3-scope="element:app"
|
|
99
122
|
onclick={() => launchApp(manifest.id)}
|
|
100
|
-
|
|
123
|
+
oncontextmenu={(e) => openAppContextMenu(e, manifest.id)}
|
|
101
124
|
>
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
</div>
|
|
125
|
+
<span class="shell-home-card-square">
|
|
126
|
+
<svg class="shell-home-card-icon"><use href="{iconsUrl}#{appearance?.icon ?? manifest.icon ?? 'box'}" /></svg>
|
|
127
|
+
</span>
|
|
128
|
+
<span class="shell-home-card-label">{appearance?.label ?? manifest.label}</span>
|
|
107
129
|
</button>
|
|
108
130
|
{/each}
|
|
109
131
|
</div>
|
|
@@ -223,26 +245,34 @@
|
|
|
223
245
|
}
|
|
224
246
|
.shell-home-grid {
|
|
225
247
|
display: grid;
|
|
226
|
-
grid-template-columns: repeat(auto-fill, minmax(
|
|
227
|
-
gap:
|
|
248
|
+
grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
|
|
249
|
+
gap: 18px 14px;
|
|
228
250
|
}
|
|
229
251
|
.shell-home-card {
|
|
230
|
-
aspect-ratio: 1 / 1;
|
|
231
252
|
display: flex;
|
|
232
253
|
flex-direction: column;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
padding:
|
|
236
|
-
background:
|
|
237
|
-
border:
|
|
238
|
-
border-radius: var(--shell-radius-md);
|
|
254
|
+
align-items: center;
|
|
255
|
+
gap: 6px;
|
|
256
|
+
padding: 0;
|
|
257
|
+
background: transparent;
|
|
258
|
+
border: none;
|
|
239
259
|
color: inherit;
|
|
240
260
|
font: inherit;
|
|
241
261
|
cursor: pointer;
|
|
262
|
+
}
|
|
263
|
+
.shell-home-card-square {
|
|
264
|
+
width: 64px;
|
|
265
|
+
height: 64px;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: center;
|
|
269
|
+
background: var(--shell-grad-bg-elevated, var(--shell-bg-elevated));
|
|
270
|
+
border: 1px solid var(--shell-border);
|
|
271
|
+
border-radius: var(--shell-radius-md);
|
|
242
272
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25), 0 1px 2px rgba(0, 0, 0, 0.15);
|
|
243
273
|
transition: transform 120ms ease, border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
244
274
|
}
|
|
245
|
-
.shell-home-card:hover {
|
|
275
|
+
.shell-home-card:hover .shell-home-card-square {
|
|
246
276
|
border-color: var(--shell-accent);
|
|
247
277
|
transform: translateY(-1px);
|
|
248
278
|
box-shadow:
|
|
@@ -252,36 +282,32 @@
|
|
|
252
282
|
}
|
|
253
283
|
.shell-home-card:focus-visible {
|
|
254
284
|
outline: none;
|
|
285
|
+
}
|
|
286
|
+
.shell-home-card:focus-visible .shell-home-card-square {
|
|
255
287
|
border-color: var(--shell-accent);
|
|
256
288
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--shell-accent) 40%, transparent);
|
|
257
289
|
}
|
|
258
|
-
.shell-home-card:active {
|
|
290
|
+
.shell-home-card:active .shell-home-card-square {
|
|
259
291
|
transform: translateY(0);
|
|
260
292
|
}
|
|
261
293
|
.shell-home-card-label {
|
|
262
294
|
font-weight: 600;
|
|
263
|
-
font-size:
|
|
295
|
+
font-size: 11px;
|
|
264
296
|
line-height: 1.2;
|
|
297
|
+
text-align: center;
|
|
265
298
|
overflow: hidden;
|
|
266
299
|
display: -webkit-box;
|
|
267
300
|
-webkit-box-orient: vertical;
|
|
268
301
|
-webkit-line-clamp: 2;
|
|
269
302
|
line-clamp: 2;
|
|
303
|
+
word-break: break-word;
|
|
270
304
|
}
|
|
271
|
-
.shell-home-card-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
font-size: 9px;
|
|
276
|
-
color: var(--shell-fg-subtle);
|
|
277
|
-
min-width: 0;
|
|
278
|
-
}
|
|
279
|
-
.shell-home-card-id {
|
|
280
|
-
overflow: hidden;
|
|
281
|
-
text-overflow: ellipsis;
|
|
282
|
-
white-space: nowrap;
|
|
305
|
+
.shell-home-card-icon {
|
|
306
|
+
width: 28px;
|
|
307
|
+
height: 28px;
|
|
308
|
+
color: var(--shell-fg);
|
|
283
309
|
}
|
|
284
|
-
.shell-home-card-
|
|
285
|
-
|
|
310
|
+
.shell-home-card--tinted .shell-home-card-square {
|
|
311
|
+
background: var(--card-color);
|
|
286
312
|
}
|
|
287
313
|
</style>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ShardContext } from '../shards/types';
|
|
2
|
+
export interface AppActionGate {
|
|
3
|
+
admin: boolean;
|
|
4
|
+
builtin: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function computeAppActionDisabled(g: AppActionGate): boolean;
|
|
7
|
+
export declare function computeAppActionLabelSuffix(g: AppActionGate): string;
|
|
8
|
+
/**
|
|
9
|
+
* Register the three element-scope app actions on the given shard context.
|
|
10
|
+
* Returns a disposer that unregisters all three (the framework expects the
|
|
11
|
+
* shard's activate to keep the disposers alive across deactivate).
|
|
12
|
+
*/
|
|
13
|
+
export declare function registerAppActions(ctx: ShardContext): () => void;
|