sh3-core 0.6.0
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/README.md +9 -0
- package/dist/Shell.svelte +283 -0
- package/dist/Shell.svelte.d.ts +5 -0
- package/dist/api.d.ts +28 -0
- package/dist/api.js +50 -0
- package/dist/app/admin/ApiKeysView.svelte +169 -0
- package/dist/app/admin/ApiKeysView.svelte.d.ts +3 -0
- package/dist/app/admin/AuthSettingsView.svelte +105 -0
- package/dist/app/admin/AuthSettingsView.svelte.d.ts +3 -0
- package/dist/app/admin/SystemView.svelte +73 -0
- package/dist/app/admin/SystemView.svelte.d.ts +3 -0
- package/dist/app/admin/UsersView.svelte +188 -0
- package/dist/app/admin/UsersView.svelte.d.ts +3 -0
- package/dist/app/admin/adminApp.d.ts +7 -0
- package/dist/app/admin/adminApp.js +25 -0
- package/dist/app/admin/adminShard.svelte.d.ts +4 -0
- package/dist/app/admin/adminShard.svelte.js +62 -0
- package/dist/app/store/InstalledView.svelte +246 -0
- package/dist/app/store/InstalledView.svelte.d.ts +3 -0
- package/dist/app/store/StoreView.svelte +522 -0
- package/dist/app/store/StoreView.svelte.d.ts +3 -0
- package/dist/app/store/storeApp.d.ts +10 -0
- package/dist/app/store/storeApp.js +26 -0
- package/dist/app/store/storeShard.svelte.d.ts +38 -0
- package/dist/app/store/storeShard.svelte.js +218 -0
- package/dist/apps/lifecycle.d.ts +42 -0
- package/dist/apps/lifecycle.js +184 -0
- package/dist/apps/registry.svelte.d.ts +40 -0
- package/dist/apps/registry.svelte.js +59 -0
- package/dist/apps/terminal/manifest.d.ts +8 -0
- package/dist/apps/terminal/manifest.js +13 -0
- package/dist/apps/terminal/terminal-app.d.ts +7 -0
- package/dist/apps/terminal/terminal-app.js +14 -0
- package/dist/apps/types.d.ts +93 -0
- package/dist/apps/types.js +10 -0
- package/dist/artifact.d.ts +32 -0
- package/dist/artifact.js +1 -0
- package/dist/assets/SH3.png +0 -0
- package/dist/assets/icons.svg +1126 -0
- package/dist/assets.d.ts +13 -0
- package/dist/auth/GuestBanner.svelte +134 -0
- package/dist/auth/GuestBanner.svelte.d.ts +3 -0
- package/dist/auth/SignInWall.svelte +203 -0
- package/dist/auth/SignInWall.svelte.d.ts +7 -0
- package/dist/auth/auth.svelte.d.ts +69 -0
- package/dist/auth/auth.svelte.js +165 -0
- package/dist/auth/index.d.ts +2 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/types.d.ts +41 -0
- package/dist/auth/types.js +6 -0
- package/dist/build.d.ts +49 -0
- package/dist/build.js +236 -0
- package/dist/contract.d.ts +20 -0
- package/dist/contract.js +28 -0
- package/dist/createShell.d.ts +24 -0
- package/dist/createShell.js +131 -0
- package/dist/documents/backends.d.ts +17 -0
- package/dist/documents/backends.js +156 -0
- package/dist/documents/config.d.ts +7 -0
- package/dist/documents/config.js +27 -0
- package/dist/documents/handle.d.ts +6 -0
- package/dist/documents/handle.js +154 -0
- package/dist/documents/http-backend.d.ts +22 -0
- package/dist/documents/http-backend.js +78 -0
- package/dist/documents/index.d.ts +6 -0
- package/dist/documents/index.js +8 -0
- package/dist/documents/notifications.d.ts +9 -0
- package/dist/documents/notifications.js +39 -0
- package/dist/documents/types.d.ts +97 -0
- package/dist/documents/types.js +12 -0
- package/dist/env/client.d.ts +44 -0
- package/dist/env/client.js +106 -0
- package/dist/env/index.d.ts +2 -0
- package/dist/env/index.js +1 -0
- package/dist/env/types.d.ts +12 -0
- package/dist/env/types.js +8 -0
- package/dist/host-entry.d.ts +13 -0
- package/dist/host-entry.js +17 -0
- package/dist/host.d.ts +15 -0
- package/dist/host.js +86 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +14 -0
- package/dist/layout/DragPreview.svelte +63 -0
- package/dist/layout/DragPreview.svelte.d.ts +3 -0
- package/dist/layout/LayoutRenderer.svelte +262 -0
- package/dist/layout/LayoutRenderer.svelte.d.ts +6 -0
- package/dist/layout/SlotContainer.svelte +140 -0
- package/dist/layout/SlotContainer.svelte.d.ts +8 -0
- package/dist/layout/SlotDropZone.svelte +122 -0
- package/dist/layout/SlotDropZone.svelte.d.ts +8 -0
- package/dist/layout/drag.svelte.d.ts +45 -0
- package/dist/layout/drag.svelte.js +200 -0
- package/dist/layout/inspection.d.ts +72 -0
- package/dist/layout/inspection.js +209 -0
- package/dist/layout/ops.d.ts +100 -0
- package/dist/layout/ops.js +310 -0
- package/dist/layout/slotHostPool.svelte.d.ts +36 -0
- package/dist/layout/slotHostPool.svelte.js +229 -0
- package/dist/layout/store.svelte.d.ts +39 -0
- package/dist/layout/store.svelte.js +153 -0
- package/dist/layout/tree-walk.d.ts +15 -0
- package/dist/layout/tree-walk.js +33 -0
- package/dist/layout/types.d.ts +108 -0
- package/dist/layout/types.js +25 -0
- package/dist/migrations/shell-rename.d.ts +16 -0
- package/dist/migrations/shell-rename.js +48 -0
- package/dist/overlays/ModalFrame.svelte +87 -0
- package/dist/overlays/ModalFrame.svelte.d.ts +10 -0
- package/dist/overlays/PopupFrame.svelte +85 -0
- package/dist/overlays/PopupFrame.svelte.d.ts +10 -0
- package/dist/overlays/ToastItem.svelte +77 -0
- package/dist/overlays/ToastItem.svelte.d.ts +9 -0
- package/dist/overlays/focusTrap.d.ts +1 -0
- package/dist/overlays/focusTrap.js +64 -0
- package/dist/overlays/modal.d.ts +9 -0
- package/dist/overlays/modal.js +141 -0
- package/dist/overlays/popup.d.ts +9 -0
- package/dist/overlays/popup.js +108 -0
- package/dist/overlays/roots.d.ts +4 -0
- package/dist/overlays/roots.js +31 -0
- package/dist/overlays/toast.d.ts +6 -0
- package/dist/overlays/toast.js +93 -0
- package/dist/overlays/types.d.ts +31 -0
- package/dist/overlays/types.js +15 -0
- package/dist/platform/index.d.ts +10 -0
- package/dist/platform/index.js +33 -0
- package/dist/platform/tauri-backend.d.ts +15 -0
- package/dist/platform/tauri-backend.js +58 -0
- package/dist/primitives/.gitkeep +0 -0
- package/dist/primitives/ResizableSplitter.svelte +333 -0
- package/dist/primitives/ResizableSplitter.svelte.d.ts +35 -0
- package/dist/primitives/TabbedPanel.svelte +305 -0
- package/dist/primitives/TabbedPanel.svelte.d.ts +50 -0
- package/dist/primitives/base.css +42 -0
- package/dist/registry/client.d.ts +74 -0
- package/dist/registry/client.js +117 -0
- package/dist/registry/index.d.ts +13 -0
- package/dist/registry/index.js +14 -0
- package/dist/registry/installer.d.ts +53 -0
- package/dist/registry/installer.js +168 -0
- package/dist/registry/integrity.d.ts +32 -0
- package/dist/registry/integrity.js +92 -0
- package/dist/registry/loader.d.ts +50 -0
- package/dist/registry/loader.js +145 -0
- package/dist/registry/schema.d.ts +47 -0
- package/dist/registry/schema.js +185 -0
- package/dist/registry/storage.d.ts +37 -0
- package/dist/registry/storage.js +101 -0
- package/dist/registry/types.d.ts +262 -0
- package/dist/registry/types.js +14 -0
- package/dist/server-shard/types.d.ts +67 -0
- package/dist/server-shard/types.js +13 -0
- package/dist/sh3core-shard/ShellHome.svelte +192 -0
- package/dist/sh3core-shard/ShellHome.svelte.d.ts +3 -0
- package/dist/sh3core-shard/ShellTitle.svelte +171 -0
- package/dist/sh3core-shard/ShellTitle.svelte.d.ts +3 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.d.ts +2 -0
- package/dist/sh3core-shard/sh3coreShard.svelte.js +53 -0
- package/dist/shards/activate.svelte.d.ts +52 -0
- package/dist/shards/activate.svelte.js +186 -0
- package/dist/shards/registry.d.ts +4 -0
- package/dist/shards/registry.js +28 -0
- package/dist/shards/types.d.ts +207 -0
- package/dist/shards/types.js +20 -0
- package/dist/shell-shard/InputLine.svelte +133 -0
- package/dist/shell-shard/InputLine.svelte.d.ts +11 -0
- package/dist/shell-shard/ScrollbackView.svelte +47 -0
- package/dist/shell-shard/ScrollbackView.svelte.d.ts +7 -0
- package/dist/shell-shard/Terminal.svelte +122 -0
- package/dist/shell-shard/Terminal.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/PromptEntry.svelte +25 -0
- package/dist/shell-shard/entries/PromptEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/RichEntry.svelte +19 -0
- package/dist/shell-shard/entries/RichEntry.svelte.d.ts +8 -0
- package/dist/shell-shard/entries/StatusEntry.svelte +22 -0
- package/dist/shell-shard/entries/StatusEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/entries/TextEntry.svelte +25 -0
- package/dist/shell-shard/entries/TextEntry.svelte.d.ts +7 -0
- package/dist/shell-shard/manifest.d.ts +2 -0
- package/dist/shell-shard/manifest.js +11 -0
- package/dist/shell-shard/protocol.d.ts +90 -0
- package/dist/shell-shard/protocol.js +11 -0
- package/dist/shell-shard/registry.d.ts +69 -0
- package/dist/shell-shard/registry.js +47 -0
- package/dist/shell-shard/rich/AppCard.svelte +25 -0
- package/dist/shell-shard/rich/AppCard.svelte.d.ts +10 -0
- package/dist/shell-shard/rich/AppsTable.svelte +29 -0
- package/dist/shell-shard/rich/AppsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/EnvTable.svelte +27 -0
- package/dist/shell-shard/rich/EnvTable.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/HelpTable.svelte +29 -0
- package/dist/shell-shard/rich/HelpTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/HistoryList.svelte +37 -0
- package/dist/shell-shard/rich/HistoryList.svelte.d.ts +9 -0
- package/dist/shell-shard/rich/ShardsTable.svelte +28 -0
- package/dist/shell-shard/rich/ShardsTable.svelte.d.ts +12 -0
- package/dist/shell-shard/rich/ViewsTable.svelte +31 -0
- package/dist/shell-shard/rich/ViewsTable.svelte.d.ts +13 -0
- package/dist/shell-shard/rich/ZoneTree.svelte +19 -0
- package/dist/shell-shard/rich/ZoneTree.svelte.d.ts +8 -0
- package/dist/shell-shard/rich/ZonesTable.svelte +27 -0
- package/dist/shell-shard/rich/ZonesTable.svelte.d.ts +11 -0
- package/dist/shell-shard/scrollback.svelte.d.ts +36 -0
- package/dist/shell-shard/scrollback.svelte.js +43 -0
- package/dist/shell-shard/session-client.svelte.d.ts +23 -0
- package/dist/shell-shard/session-client.svelte.js +120 -0
- package/dist/shell-shard/shellShard.svelte.d.ts +2 -0
- package/dist/shell-shard/shellShard.svelte.js +139 -0
- package/dist/shell-shard/verbs/apps.d.ts +3 -0
- package/dist/shell-shard/verbs/apps.js +50 -0
- package/dist/shell-shard/verbs/clear.d.ts +2 -0
- package/dist/shell-shard/verbs/clear.js +7 -0
- package/dist/shell-shard/verbs/help.d.ts +2 -0
- package/dist/shell-shard/verbs/help.js +21 -0
- package/dist/shell-shard/verbs/history.d.ts +2 -0
- package/dist/shell-shard/verbs/history.js +20 -0
- package/dist/shell-shard/verbs/index.d.ts +2 -0
- package/dist/shell-shard/verbs/index.js +29 -0
- package/dist/shell-shard/verbs/session.d.ts +5 -0
- package/dist/shell-shard/verbs/session.js +65 -0
- package/dist/shell-shard/verbs/shards.d.ts +2 -0
- package/dist/shell-shard/verbs/shards.js +14 -0
- package/dist/shell-shard/verbs/views.d.ts +4 -0
- package/dist/shell-shard/verbs/views.js +90 -0
- package/dist/shell-shard/verbs/zones.d.ts +3 -0
- package/dist/shell-shard/verbs/zones.js +38 -0
- package/dist/shellRuntime.svelte.d.ts +27 -0
- package/dist/shellRuntime.svelte.js +27 -0
- package/dist/state/backends.d.ts +26 -0
- package/dist/state/backends.js +99 -0
- package/dist/state/manage.d.ts +14 -0
- package/dist/state/manage.js +40 -0
- package/dist/state/types.d.ts +55 -0
- package/dist/state/types.js +17 -0
- package/dist/state/zones.svelte.d.ts +53 -0
- package/dist/state/zones.svelte.js +141 -0
- package/dist/theme.d.ts +28 -0
- package/dist/theme.js +92 -0
- package/dist/tokens.css +102 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +2 -0
- package/package.json +60 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Contribution registry — phase 4 stub.
|
|
3
|
+
*
|
|
4
|
+
* Tracks which ViewFactory answers a given viewId. In this phase the
|
|
5
|
+
* registry is a flat module-level Map with no awareness of shard identity;
|
|
6
|
+
* writes come from `activateShard` which additionally remembers which
|
|
7
|
+
* viewIds a given shard registered so they can be torn down in
|
|
8
|
+
* `deactivateShard`.
|
|
9
|
+
*
|
|
10
|
+
* The shape of this registry is deliberately narrow so later phases can
|
|
11
|
+
* expand it without breaking callers:
|
|
12
|
+
* - Resolution by viewId is the only query slots need.
|
|
13
|
+
* - Commands, toolbar items, menus, hotkeys get their own sibling maps
|
|
14
|
+
* (one per contribution kind) when those kinds land.
|
|
15
|
+
*/
|
|
16
|
+
const views = new Map();
|
|
17
|
+
export function registerView(viewId, factory) {
|
|
18
|
+
if (views.has(viewId)) {
|
|
19
|
+
throw new Error(`View "${viewId}" is already registered`);
|
|
20
|
+
}
|
|
21
|
+
views.set(viewId, factory);
|
|
22
|
+
}
|
|
23
|
+
export function getView(viewId) {
|
|
24
|
+
return views.get(viewId);
|
|
25
|
+
}
|
|
26
|
+
export function unregisterView(viewId) {
|
|
27
|
+
views.delete(viewId);
|
|
28
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import type { StateZones } from '../state/zones.svelte';
|
|
2
|
+
import type { ZoneSchema, ZoneManager } from '../state/types';
|
|
3
|
+
import type { DocumentHandle, DocumentHandleOptions } from '../documents/types';
|
|
4
|
+
import type { EnvState } from '../env/types';
|
|
5
|
+
/**
|
|
6
|
+
* The object returned by `ViewFactory.mount`. The framework calls
|
|
7
|
+
* `unmount()` when the slot goes away, and `onResize(w, h)` whenever the
|
|
8
|
+
* slot's container changes size (including splitter drags and window
|
|
9
|
+
* resize). `remountOnMove` is the opt-out from the re-parenting-survival
|
|
10
|
+
* contract (rare, GL-edge-case shards); phase 4 does not exercise it.
|
|
11
|
+
*/
|
|
12
|
+
export interface ViewHandle {
|
|
13
|
+
/** Called by the framework when the slot is removed from the layout. Release all resources here. */
|
|
14
|
+
unmount(): void;
|
|
15
|
+
/** Optional callback invoked when the slot's container dimensions change. */
|
|
16
|
+
onResize?(width: number, height: number): void;
|
|
17
|
+
/**
|
|
18
|
+
* When true the framework will unmount and remount this view if its slot
|
|
19
|
+
* moves to a different DOM parent (e.g. a splitter drag). Set to false
|
|
20
|
+
* (or omit) if the view can survive DOM re-parenting without reinitializing.
|
|
21
|
+
*/
|
|
22
|
+
remountOnMove?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Closability mode for this view instance.
|
|
25
|
+
* - undefined / false: non-closable (no close button rendered).
|
|
26
|
+
* - true: pure close — instant removal, no confirmation.
|
|
27
|
+
* - { canClose() }: guarded close — layout engine awaits the promise;
|
|
28
|
+
* resolve true to allow, false to cancel.
|
|
29
|
+
*/
|
|
30
|
+
closable?: boolean | {
|
|
31
|
+
canClose(): Promise<boolean>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Context passed to `ViewFactory.mount` so the view knows which layout
|
|
36
|
+
* instance it is and can push reactive metadata back to the tab strip.
|
|
37
|
+
*/
|
|
38
|
+
export interface MountContext {
|
|
39
|
+
/** Stable identifier for the slot this view is mounted into. */
|
|
40
|
+
slotId: string;
|
|
41
|
+
/** The view id that was used to look up this factory. */
|
|
42
|
+
viewId: string;
|
|
43
|
+
/** Initial label for the tab; may be updated by the view via future API. */
|
|
44
|
+
label: string;
|
|
45
|
+
/**
|
|
46
|
+
* Push dirty-state to the tab strip. The framework renders a dirty
|
|
47
|
+
* indicator (filled dot) on the tab when true, clears it when false.
|
|
48
|
+
* Call this whenever the view's save-state changes.
|
|
49
|
+
*/
|
|
50
|
+
setDirty(dirty: boolean): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The shard-side adapter that knows how to bring a view to life inside a
|
|
54
|
+
* given HTMLElement. The container is owned by the framework (the slot);
|
|
55
|
+
* the factory writes into it and returns a ViewHandle.
|
|
56
|
+
*/
|
|
57
|
+
export interface ViewFactory {
|
|
58
|
+
/**
|
|
59
|
+
* Mount the view into `container`. The container element is owned and sized
|
|
60
|
+
* by the framework; the factory should not modify its dimensions or position.
|
|
61
|
+
* Returns a `ViewHandle` the framework uses to communicate with the view.
|
|
62
|
+
*/
|
|
63
|
+
mount(container: HTMLElement, context: MountContext): ViewHandle;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Static description of a view a shard provides. Lives inside
|
|
67
|
+
* `ShardManifest.views` so apps and tooling can enumerate a shard's views
|
|
68
|
+
* without running `activate()`. Phase 8 only uses `id` and `label`; `icon`
|
|
69
|
+
* is reserved and not rendered yet.
|
|
70
|
+
*/
|
|
71
|
+
export interface ViewDeclaration {
|
|
72
|
+
/** Unique view id within this shard. Used to look up the factory at mount time. */
|
|
73
|
+
id: string;
|
|
74
|
+
/** Human-readable label used in tab strips and launchers. */
|
|
75
|
+
label: string;
|
|
76
|
+
/** Optional icon hint (reserved; not yet rendered in phase 8). */
|
|
77
|
+
icon?: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Static description of a shard. Declared once and read by the framework
|
|
81
|
+
* before `activate` runs, so the shell can enumerate a shard's capabilities
|
|
82
|
+
* without executing any shard code.
|
|
83
|
+
*/
|
|
84
|
+
export interface ShardManifest {
|
|
85
|
+
/** Unique shard identifier. Used as the state namespace and view id prefix. */
|
|
86
|
+
id: string;
|
|
87
|
+
/** Human-readable display name. */
|
|
88
|
+
label: string;
|
|
89
|
+
/** Semver version string for the shard package. */
|
|
90
|
+
version: string;
|
|
91
|
+
/**
|
|
92
|
+
* Static list of the view ids this shard provides. Every id listed here
|
|
93
|
+
* must be backed by a `ctx.registerView(id, factory)` call from within
|
|
94
|
+
* `activate()`; the framework verifies this after `activate` returns and
|
|
95
|
+
* throws on any missing factory. Phase 8 adds this field; shards that
|
|
96
|
+
* predate phase 8 must be updated to declare their views here.
|
|
97
|
+
*/
|
|
98
|
+
views: ViewDeclaration[];
|
|
99
|
+
/**
|
|
100
|
+
* Optional filename of a server-side bundle for this shard. When present,
|
|
101
|
+
* sh3-server loads the bundle at boot and mounts its routes at
|
|
102
|
+
* `/api/<shard-id>/`. The server bundle runs in Node with full access.
|
|
103
|
+
* Only relevant for shards installed via the package store; framework-
|
|
104
|
+
* shipped shards do not use this field.
|
|
105
|
+
*/
|
|
106
|
+
serverBundle?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Optional permissions this shard requests beyond the default sandbox.
|
|
109
|
+
* Declared in the manifest and surfaced to the user at install time.
|
|
110
|
+
* Currently recognized: `'state:manage'` — cross-shard zone access.
|
|
111
|
+
*/
|
|
112
|
+
permissions?: string[];
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Handed to `shard.activate`. The shard uses it to declare state and
|
|
116
|
+
* register contributions. `state` is pre-bound to the shard's id so the
|
|
117
|
+
* shard never has to pass its own id through — the framework guarantees
|
|
118
|
+
* isolation by construction.
|
|
119
|
+
*/
|
|
120
|
+
export interface ShardContext {
|
|
121
|
+
/**
|
|
122
|
+
* Declare the state zones this shard uses and receive a live reactive
|
|
123
|
+
* object. The shard id is baked in — shards never pass their own id.
|
|
124
|
+
* Persistent zones (`workspace`, `user`) are hydrated before the call
|
|
125
|
+
* returns.
|
|
126
|
+
*
|
|
127
|
+
* @param schema - Zone names mapped to their default values.
|
|
128
|
+
*/
|
|
129
|
+
state<T extends ZoneSchema>(schema: T): StateZones<T>;
|
|
130
|
+
/**
|
|
131
|
+
* Register a view factory for a view id declared in the shard manifest.
|
|
132
|
+
* Must be called for every id listed in `manifest.views` during `activate`.
|
|
133
|
+
*
|
|
134
|
+
* @param viewId - Must match an entry in `manifest.views`.
|
|
135
|
+
* @param factory - The adapter that mounts the view into a container element.
|
|
136
|
+
*/
|
|
137
|
+
registerView(viewId: string, factory: ViewFactory): void;
|
|
138
|
+
/** Obtain a file-oriented document handle scoped to this shard. */
|
|
139
|
+
documents(options: DocumentHandleOptions): DocumentHandle;
|
|
140
|
+
/**
|
|
141
|
+
* Declare environment state for this shard and receive a hydrated snapshot.
|
|
142
|
+
* Env state is server-authoritative, fetched once at activation, and
|
|
143
|
+
* shallow-merged with defaults (new fields get defaults even if the server
|
|
144
|
+
* has an older entry). Read-only for non-admin sessions.
|
|
145
|
+
*
|
|
146
|
+
* @param defaults - Default values for each env state field.
|
|
147
|
+
* @returns A reactive proxy of the env state.
|
|
148
|
+
*/
|
|
149
|
+
env<T extends Record<string, unknown>>(defaults: T): EnvState<T>;
|
|
150
|
+
/**
|
|
151
|
+
* Update this shard's environment state on the server. Merges the patch
|
|
152
|
+
* into the current env state, writes to the server, and updates the local
|
|
153
|
+
* reactive proxy. Throws if the session is not admin-elevated.
|
|
154
|
+
*
|
|
155
|
+
* @param patch - Partial env state to merge.
|
|
156
|
+
*/
|
|
157
|
+
envUpdate<T extends Record<string, unknown>>(patch: Partial<T>): Promise<void>;
|
|
158
|
+
/** Whether the current session has admin privileges. */
|
|
159
|
+
isAdmin: boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Cross-shard zone management API. Only present when the shard's
|
|
162
|
+
* manifest declares the `'state:manage'` permission. Check with
|
|
163
|
+
* `if (ctx.zones)` before use.
|
|
164
|
+
*/
|
|
165
|
+
zones?: ZoneManager;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
169
|
+
* Each shard activates once, receives a `ShardContext`, and registers views
|
|
170
|
+
* and other capabilities that the shell and apps can use. Shard lifecycle is
|
|
171
|
+
* separate from view lifecycle — a shard may be active with no views visible.
|
|
172
|
+
*/
|
|
173
|
+
export interface Shard {
|
|
174
|
+
/** Static description of this shard's identity and declared contributions. */
|
|
175
|
+
manifest: ShardManifest;
|
|
176
|
+
/**
|
|
177
|
+
* Run once when the shard is activated. Use `ctx` to declare state zones,
|
|
178
|
+
* register view factories, and obtain document handles. Must register a
|
|
179
|
+
* factory for every view id declared in `manifest.views`.
|
|
180
|
+
*/
|
|
181
|
+
activate(ctx: ShardContext): void | Promise<void>;
|
|
182
|
+
/**
|
|
183
|
+
* Optional self-starting hook. A shard that defines `autostart` is
|
|
184
|
+
* eagerly activated by the framework at boot (right after the register
|
|
185
|
+
* pass finishes) instead of waiting for an app to require it. `activate`
|
|
186
|
+
* runs first; `autostart` runs immediately after and may take imperative
|
|
187
|
+
* action — docking its own views into the active layout, opening a
|
|
188
|
+
* modal, subscribing to framework state, etc. The `__sh3core__` pseudo-
|
|
189
|
+
* shard uses this with a no-op body so its activation path is uniform
|
|
190
|
+
* with other self-starting shards. Diagnostic-style shards use it to
|
|
191
|
+
* do real work.
|
|
192
|
+
*/
|
|
193
|
+
autostart?(ctx: ShardContext): void | Promise<void>;
|
|
194
|
+
/** Optional cleanup hook called when the shard is deactivated. Release timers, subscriptions, and external resources here. */
|
|
195
|
+
deactivate?(): void | Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Called when the owning app is suspended (Home button). The shard
|
|
198
|
+
* remains active; its views and state are preserved. Return `false`
|
|
199
|
+
* (sync or async) to cancel the navigation.
|
|
200
|
+
*/
|
|
201
|
+
suspend?(): void | false | Promise<void | false>;
|
|
202
|
+
/**
|
|
203
|
+
* Called when the owning app resumes from Home. Receives the same
|
|
204
|
+
* `ShardContext` that `activate` received.
|
|
205
|
+
*/
|
|
206
|
+
resume?(ctx: ShardContext): void | Promise<void>;
|
|
207
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shard contract — minimum viable draft for phase 4.
|
|
3
|
+
*
|
|
4
|
+
* A shard is a self-contained module that contributes capabilities (views,
|
|
5
|
+
* commands, services, …) to the shell. See docs/design/shards.md for the
|
|
6
|
+
* full design; phase 4 implements only the pieces needed to mount a view
|
|
7
|
+
* into a layout slot:
|
|
8
|
+
*
|
|
9
|
+
* - A shard declares a manifest.
|
|
10
|
+
* - `activate(ctx)` runs once, receives a ShardContext, and registers
|
|
11
|
+
* whichever contributions it wants the shell to know about.
|
|
12
|
+
* - A ViewFactory knows how to mount a view into a raw HTMLElement and
|
|
13
|
+
* return a handle the framework uses to unmount / notify of resizes.
|
|
14
|
+
*
|
|
15
|
+
* Deferred to later phases: bus scoping, command/toolbar/menu/hotkey
|
|
16
|
+
* registration, modal provider contributions, background services, lazy
|
|
17
|
+
* activation events. They'll slot into `ShardContext` as new `register*`
|
|
18
|
+
* methods without disturbing the phase-4 shape.
|
|
19
|
+
*/
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SessionClient } from './session-client.svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
cwd: string;
|
|
6
|
+
locked: boolean; // true while a process is running
|
|
7
|
+
history: string[]; // persisted history, newest last
|
|
8
|
+
session: SessionClient;
|
|
9
|
+
onSubmit: (line: string) => void; // called with the raw entered line
|
|
10
|
+
}
|
|
11
|
+
let { cwd, locked, history, session, onSubmit }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let draft = $state('');
|
|
14
|
+
let historyIndex = $state<number | null>(null); // null = live draft
|
|
15
|
+
let savedDraft = $state(''); // restored when user returns from history
|
|
16
|
+
|
|
17
|
+
let input: HTMLInputElement | null = $state(null);
|
|
18
|
+
|
|
19
|
+
function submit() {
|
|
20
|
+
if (locked) return;
|
|
21
|
+
const line = draft;
|
|
22
|
+
if (!line.trim()) return;
|
|
23
|
+
draft = '';
|
|
24
|
+
historyIndex = null;
|
|
25
|
+
onSubmit(line);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function navHistoryUp() {
|
|
29
|
+
if (history.length === 0) return;
|
|
30
|
+
if (historyIndex === null) {
|
|
31
|
+
savedDraft = draft;
|
|
32
|
+
historyIndex = history.length - 1;
|
|
33
|
+
} else if (historyIndex > 0) {
|
|
34
|
+
historyIndex--;
|
|
35
|
+
}
|
|
36
|
+
draft = history[historyIndex] ?? '';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function navHistoryDown() {
|
|
40
|
+
if (historyIndex === null) return;
|
|
41
|
+
if (historyIndex < history.length - 1) {
|
|
42
|
+
historyIndex++;
|
|
43
|
+
draft = history[historyIndex];
|
|
44
|
+
} else {
|
|
45
|
+
historyIndex = null;
|
|
46
|
+
draft = savedDraft;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function onKeyDown(e: KeyboardEvent) {
|
|
51
|
+
if (locked) {
|
|
52
|
+
if (e.ctrlKey && e.key === 'c') {
|
|
53
|
+
e.preventDefault();
|
|
54
|
+
session.send({ t: 'signal', sig: 'SIGINT' });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (e.ctrlKey && e.key === 'd') {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
session.send({ t: 'signal', sig: 'EOF' });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// Drop all other input while locked
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (e.key === 'Enter') {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
submit();
|
|
70
|
+
} else if (e.key === 'ArrowUp') {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
navHistoryUp();
|
|
73
|
+
} else if (e.key === 'ArrowDown') {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
navHistoryDown();
|
|
76
|
+
} else if (e.ctrlKey && e.key === 'c') {
|
|
77
|
+
e.preventDefault();
|
|
78
|
+
draft = '';
|
|
79
|
+
historyIndex = null;
|
|
80
|
+
} else if (e.ctrlKey && e.key === 'l') {
|
|
81
|
+
e.preventDefault();
|
|
82
|
+
draft = 'clear';
|
|
83
|
+
submit();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Re-focus the input when the locked state flips to false
|
|
88
|
+
$effect(() => {
|
|
89
|
+
if (!locked && input) {
|
|
90
|
+
input.focus();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
</script>
|
|
94
|
+
|
|
95
|
+
<div class="shell-input" class:locked>
|
|
96
|
+
<span class="shell-input-cwd">{cwd}</span>
|
|
97
|
+
<span class="shell-input-arrow">❯</span>
|
|
98
|
+
<input
|
|
99
|
+
bind:this={input}
|
|
100
|
+
bind:value={draft}
|
|
101
|
+
type="text"
|
|
102
|
+
disabled={locked}
|
|
103
|
+
onkeydown={onKeyDown}
|
|
104
|
+
spellcheck="false"
|
|
105
|
+
autocomplete="off"
|
|
106
|
+
autocapitalize="off"
|
|
107
|
+
class="shell-input-field"
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<style>
|
|
112
|
+
.shell-input {
|
|
113
|
+
display: flex;
|
|
114
|
+
gap: 8px;
|
|
115
|
+
padding: 4px 8px;
|
|
116
|
+
border-top: 1px solid var(--shell-border, #333);
|
|
117
|
+
font-family: var(--shell-font-mono, monospace);
|
|
118
|
+
}
|
|
119
|
+
.shell-input-cwd { color: var(--shell-fg-muted, #888); }
|
|
120
|
+
.shell-input-arrow { color: var(--shell-accent, #6cf); }
|
|
121
|
+
.shell-input-field {
|
|
122
|
+
flex: 1 1 auto;
|
|
123
|
+
background: transparent;
|
|
124
|
+
border: 0;
|
|
125
|
+
outline: 0;
|
|
126
|
+
color: var(--shell-fg, #ddd);
|
|
127
|
+
font: inherit;
|
|
128
|
+
}
|
|
129
|
+
.shell-input.locked .shell-input-field {
|
|
130
|
+
opacity: 0.5;
|
|
131
|
+
cursor: default;
|
|
132
|
+
}
|
|
133
|
+
</style>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionClient } from './session-client.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
cwd: string;
|
|
4
|
+
locked: boolean;
|
|
5
|
+
history: string[];
|
|
6
|
+
session: SessionClient;
|
|
7
|
+
onSubmit: (line: string) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const InputLine: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type InputLine = ReturnType<typeof InputLine>;
|
|
11
|
+
export default InputLine;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Scrollback } from './scrollback.svelte';
|
|
3
|
+
import TextEntry from './entries/TextEntry.svelte';
|
|
4
|
+
import PromptEntry from './entries/PromptEntry.svelte';
|
|
5
|
+
import StatusEntry from './entries/StatusEntry.svelte';
|
|
6
|
+
import RichEntry from './entries/RichEntry.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
scrollback: Scrollback;
|
|
10
|
+
}
|
|
11
|
+
let { scrollback }: Props = $props();
|
|
12
|
+
|
|
13
|
+
let container: HTMLDivElement | null = $state(null);
|
|
14
|
+
|
|
15
|
+
// Auto-scroll to bottom on new entries
|
|
16
|
+
$effect(() => {
|
|
17
|
+
// Depend on entries length so the effect re-runs
|
|
18
|
+
const _len = scrollback.entries.length;
|
|
19
|
+
void _len;
|
|
20
|
+
if (container) {
|
|
21
|
+
container.scrollTop = container.scrollHeight;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<div class="shell-scrollback" bind:this={container}>
|
|
27
|
+
{#each scrollback.entries as entry (entry.id)}
|
|
28
|
+
{#if entry.kind === 'text'}
|
|
29
|
+
<TextEntry stream={entry.stream} chunks={entry.chunks} />
|
|
30
|
+
{:else if entry.kind === 'prompt'}
|
|
31
|
+
<PromptEntry cwd={entry.cwd} line={entry.line} />
|
|
32
|
+
{:else if entry.kind === 'status'}
|
|
33
|
+
<StatusEntry text={entry.text} level={entry.level} />
|
|
34
|
+
{:else if entry.kind === 'rich'}
|
|
35
|
+
<RichEntry component={entry.component} componentProps={entry.props} />
|
|
36
|
+
{/if}
|
|
37
|
+
{/each}
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style>
|
|
41
|
+
.shell-scrollback {
|
|
42
|
+
flex: 1 1 auto;
|
|
43
|
+
overflow-y: auto;
|
|
44
|
+
background: var(--shell-bg, #111);
|
|
45
|
+
color: var(--shell-fg, #ddd);
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Scrollback } from './scrollback.svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
scrollback: Scrollback;
|
|
4
|
+
}
|
|
5
|
+
declare const ScrollbackView: import("svelte").Component<Props, {}, "">;
|
|
6
|
+
type ScrollbackView = ReturnType<typeof ScrollbackView>;
|
|
7
|
+
export default ScrollbackView;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy, untrack } from 'svelte';
|
|
3
|
+
import { Scrollback } from './scrollback.svelte';
|
|
4
|
+
import ScrollbackView from './ScrollbackView.svelte';
|
|
5
|
+
import InputLine from './InputLine.svelte';
|
|
6
|
+
import { SessionClient } from './session-client.svelte';
|
|
7
|
+
import { VerbRegistry, type ShellApi } from './registry';
|
|
8
|
+
import { registerV1Verbs } from './verbs';
|
|
9
|
+
import type { ServerMessage } from './protocol';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
shell: ShellApi;
|
|
13
|
+
wsUrl: string;
|
|
14
|
+
}
|
|
15
|
+
let { shell, wsUrl }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const scrollback = new Scrollback();
|
|
18
|
+
// wsUrl is a prop read at construction only. untrack prevents Svelte 5's
|
|
19
|
+
// "referenced outside a closure" warning; the URL never changes at runtime.
|
|
20
|
+
const session = untrack(() => new SessionClient(wsUrl));
|
|
21
|
+
const registry = new VerbRegistry();
|
|
22
|
+
registerV1Verbs(registry);
|
|
23
|
+
|
|
24
|
+
let locked = $state(false);
|
|
25
|
+
|
|
26
|
+
async function dispatch(line: string): Promise<void> {
|
|
27
|
+
const resolution = registry.resolve(line);
|
|
28
|
+
if (resolution.kind === 'local') {
|
|
29
|
+
// Log locally-dispatched verbs for shared history
|
|
30
|
+
session.send({ t: 'history-log', line });
|
|
31
|
+
scrollback.push({
|
|
32
|
+
kind: 'prompt',
|
|
33
|
+
cwd: session.cwd,
|
|
34
|
+
line,
|
|
35
|
+
ts: Date.now(),
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
await resolution.verb.run({
|
|
39
|
+
shell,
|
|
40
|
+
scrollback,
|
|
41
|
+
session,
|
|
42
|
+
cwd: session.cwd,
|
|
43
|
+
dispatch,
|
|
44
|
+
}, resolution.args);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
scrollback.push({
|
|
47
|
+
kind: 'status',
|
|
48
|
+
text: `shell: verb ${resolution.verb.name} threw — ${(err as Error).message}`,
|
|
49
|
+
level: 'error',
|
|
50
|
+
ts: Date.now(),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Forward to server
|
|
55
|
+
session.send({ t: 'submit', line: resolution.line });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function handleServerMessage(msg: ServerMessage) {
|
|
60
|
+
if (msg.t !== 'event') return;
|
|
61
|
+
const e = msg.event;
|
|
62
|
+
switch (e.kind) {
|
|
63
|
+
case 'prompt':
|
|
64
|
+
scrollback.push({ kind: 'prompt', cwd: e.cwd, line: e.line, ts: e.ts });
|
|
65
|
+
locked = true;
|
|
66
|
+
break;
|
|
67
|
+
case 'stdout':
|
|
68
|
+
scrollback.push({ kind: 'text', stream: 'stdout', chunks: [e.data], ts: e.ts });
|
|
69
|
+
break;
|
|
70
|
+
case 'stderr':
|
|
71
|
+
scrollback.push({ kind: 'text', stream: 'stderr', chunks: [e.data], ts: e.ts });
|
|
72
|
+
break;
|
|
73
|
+
case 'exit':
|
|
74
|
+
scrollback.push({
|
|
75
|
+
kind: 'status',
|
|
76
|
+
text: e.signal
|
|
77
|
+
? `shell: process exited (${e.signal})`
|
|
78
|
+
: `shell: process exited (${e.code ?? 0})`,
|
|
79
|
+
level: e.code === 0 || e.code === null ? 'info' : 'error',
|
|
80
|
+
ts: e.ts,
|
|
81
|
+
});
|
|
82
|
+
locked = false;
|
|
83
|
+
break;
|
|
84
|
+
case 'status':
|
|
85
|
+
scrollback.push({ kind: 'status', text: e.text, level: e.level, ts: e.ts });
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
let unsub: (() => void) | null = null;
|
|
91
|
+
|
|
92
|
+
onMount(() => {
|
|
93
|
+
unsub = session.onMessage(handleServerMessage);
|
|
94
|
+
session.connect();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
onDestroy(() => {
|
|
98
|
+
unsub?.();
|
|
99
|
+
session.close();
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
|
|
103
|
+
<div class="shell-terminal">
|
|
104
|
+
<ScrollbackView {scrollback} />
|
|
105
|
+
<InputLine
|
|
106
|
+
cwd={session.cwd}
|
|
107
|
+
{locked}
|
|
108
|
+
history={session.history}
|
|
109
|
+
{session}
|
|
110
|
+
onSubmit={dispatch}
|
|
111
|
+
/>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
.shell-terminal {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
height: 100%;
|
|
119
|
+
background: var(--shell-bg, #111);
|
|
120
|
+
color: var(--shell-fg, #ddd);
|
|
121
|
+
}
|
|
122
|
+
</style>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
cwd: string;
|
|
4
|
+
line: string;
|
|
5
|
+
}
|
|
6
|
+
let { cwd, line }: Props = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="shell-prompt">
|
|
10
|
+
<span class="shell-prompt-cwd">{cwd}</span>
|
|
11
|
+
<span class="shell-prompt-arrow">❯</span>
|
|
12
|
+
<span class="shell-prompt-line">{line}</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.shell-prompt {
|
|
17
|
+
padding: 4px 8px 0 8px;
|
|
18
|
+
font-family: var(--shell-font-mono, monospace);
|
|
19
|
+
display: flex;
|
|
20
|
+
gap: 8px;
|
|
21
|
+
}
|
|
22
|
+
.shell-prompt-cwd { color: var(--shell-fg-muted, #888); }
|
|
23
|
+
.shell-prompt-arrow { color: var(--shell-accent, #6cf); }
|
|
24
|
+
.shell-prompt-line { color: var(--shell-fg, #ddd); }
|
|
25
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Component } from 'svelte';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
component: Component;
|
|
6
|
+
componentProps: Record<string, unknown>;
|
|
7
|
+
}
|
|
8
|
+
let { component: C, componentProps }: Props = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<div class="shell-rich">
|
|
12
|
+
<C {...componentProps} />
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<style>
|
|
16
|
+
.shell-rich {
|
|
17
|
+
padding: 4px 8px;
|
|
18
|
+
}
|
|
19
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
text: string;
|
|
4
|
+
level: 'info' | 'warn' | 'error';
|
|
5
|
+
}
|
|
6
|
+
let { text, level }: Props = $props();
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<div class="shell-status" class:info={level === 'info'} class:warn={level === 'warn'} class:error={level === 'error'}>
|
|
10
|
+
{text}
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<style>
|
|
14
|
+
.shell-status {
|
|
15
|
+
padding: 2px 8px;
|
|
16
|
+
font-family: var(--shell-font-mono, monospace);
|
|
17
|
+
font-style: italic;
|
|
18
|
+
}
|
|
19
|
+
.shell-status.info { color: var(--shell-fg-muted, #888); }
|
|
20
|
+
.shell-status.warn { color: var(--shell-fg-warn, #fc6); }
|
|
21
|
+
.shell-status.error { color: var(--shell-fg-error, #f88); }
|
|
22
|
+
</style>
|