sh3-core 0.15.4 → 0.16.1
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/BrandSlot.svelte +9 -9
- package/dist/{Shell.svelte → Sh3.svelte} +65 -65
- package/dist/Sh3.svelte.d.ts +5 -0
- package/dist/__test__/render.js +1 -1
- package/dist/actions/ActionPanel.svelte +3 -3
- package/dist/actions/CommandPalette.svelte +2 -2
- package/dist/actions/MenuBar.svelte +1 -1
- package/dist/actions/MenuBar.test.js +1 -1
- package/dist/actions/MenuButton.svelte +3 -3
- package/dist/actions/dispatcher.svelte.js +1 -1
- package/dist/actions/listActive.js +1 -1
- package/dist/actions/listeners.js +6 -6
- package/dist/actions/selection.svelte.d.ts +2 -2
- package/dist/actions/selection.svelte.js +4 -4
- package/dist/actions/selection.test.js +1 -1
- package/dist/actions/{shellActions.test.js → sh3Actions.test.js} +14 -14
- package/dist/actions/state.svelte.d.ts +3 -3
- package/dist/actions/state.svelte.js +3 -3
- package/dist/actions/syncMountedViewIds.test.js +1 -1
- package/dist/actions/types.d.ts +1 -1
- package/dist/api.d.ts +5 -5
- package/dist/api.js +7 -7
- package/dist/app/admin/ApiKeysView.svelte +9 -9
- package/dist/app/admin/AuthSettingsView.svelte +6 -6
- package/dist/app/admin/SystemView.svelte +12 -12
- package/dist/app/admin/UsersView.svelte +8 -8
- package/dist/app/store/AppUpdateAvailableModal.svelte +14 -14
- package/dist/app/store/PermissionConfirmModal.svelte +20 -20
- package/dist/app/store/StoreView.svelte +51 -51
- package/dist/app/store/UninstallAppDialog.svelte +14 -14
- package/dist/app/store/storeShard.svelte.js +1 -1
- package/dist/app/store/verbs.js +1 -1
- package/dist/apps/lifecycle.d.ts +1 -1
- package/dist/apps/lifecycle.js +4 -4
- package/dist/apps/lifecycle.test.js +5 -5
- package/dist/apps/registry.svelte.d.ts +2 -2
- package/dist/apps/registry.svelte.js +3 -3
- package/dist/apps/types.d.ts +1 -1
- package/dist/auth/GuestBanner.svelte +19 -19
- package/dist/auth/SignInWall.svelte +17 -17
- package/dist/auth/auth.svelte.d.ts +1 -1
- package/dist/auth/auth.svelte.js +1 -1
- package/dist/boot/satellitePayload.js +1 -1
- package/dist/build.d.ts +27 -0
- package/dist/build.js +59 -1
- package/dist/build.test.js +31 -0
- package/dist/color/api.d.ts +1 -1
- package/dist/color/api.js +1 -1
- package/dist/color/{shell-api.js → sh3-api.js} +3 -3
- package/dist/conflicts/ConflictModal.svelte +1 -1
- package/dist/conflicts/ConflictModal.svelte.d.ts +1 -1
- package/dist/conflicts/DetailView.svelte +19 -19
- package/dist/conflicts/PromptView.svelte +9 -9
- package/dist/conflicts/adapter-documents.js +1 -1
- package/dist/conflicts/api.d.ts +1 -1
- package/dist/conflicts/api.js +2 -2
- package/dist/conflicts/renderer-registry.js +1 -1
- package/dist/conflicts/renderers/MetaOnlyRenderer.svelte +8 -8
- package/dist/conflicts/renderers/TextDiffRenderer.svelte +15 -15
- package/dist/conflicts/renderers/index.js +1 -1
- package/dist/conflicts/resolve-primitive.js +1 -1
- package/dist/conflicts/{shell-api.js → sh3-api.js} +2 -2
- package/dist/contributions/registry.js +1 -1
- package/dist/createShell.d.ts +2 -2
- package/dist/createShell.js +6 -6
- package/dist/host-entry.d.ts +1 -1
- package/dist/host-entry.js +2 -2
- package/dist/host.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/keys/ConsentDialog.svelte +15 -15
- package/dist/keys/consent.svelte.d.ts +2 -2
- package/dist/keys/consent.svelte.js +3 -3
- package/dist/keys/revocation-bus.svelte.d.ts +3 -3
- package/dist/keys/revocation-bus.svelte.js +3 -3
- package/dist/layout/DragPreview.svelte +9 -9
- package/dist/layout/LayoutRenderer.browser.test.js +2 -2
- package/dist/layout/LayoutRenderer.svelte +4 -4
- package/dist/layout/SlotContainer.svelte +12 -12
- package/dist/layout/SlotDropZone.svelte +2 -2
- package/dist/layout/inspection.d.ts +1 -1
- package/dist/layout/inspection.js +1 -1
- package/dist/layout/store.svelte.js +3 -3
- package/dist/layout/types.d.ts +1 -1
- package/dist/layouts-shard/LayoutSaveModal.svelte +20 -20
- package/dist/layouts-shard/LayoutsSection.svelte +11 -11
- package/dist/layouts-shard/filter.test.js +3 -3
- package/dist/layouts-shard/layoutsApi.test.js +3 -3
- package/dist/layouts-shard/layoutsShard.svelte.test.js +7 -7
- package/dist/migrations/mode-id-rename.js +2 -2
- package/dist/overlays/ConfirmDialog.svelte +8 -8
- package/dist/overlays/EntityAppearanceModal.svelte +19 -19
- package/dist/overlays/FloatFrame.svelte +20 -20
- package/dist/overlays/FloatLayer.svelte +1 -1
- package/dist/overlays/ModalFrame.svelte +5 -5
- package/dist/overlays/PopupFrame.svelte +4 -4
- package/dist/overlays/ToastItem.svelte +12 -12
- package/dist/overlays/float.d.ts +2 -2
- package/dist/overlays/float.js +3 -3
- package/dist/overlays/float.test.js +6 -6
- package/dist/overlays/modal.js +1 -1
- package/dist/overlays/modal.test.js +2 -2
- package/dist/overlays/parentHost.js +2 -2
- package/dist/overlays/parentHost.test.js +5 -5
- package/dist/overlays/popup.js +2 -2
- package/dist/overlays/popup.test.js +2 -2
- package/dist/overlays/roots.js +4 -4
- package/dist/overlays/toast.js +1 -1
- package/dist/overlays/types.js +3 -3
- package/dist/primitives/Button.svelte +16 -16
- package/dist/primitives/Collapsible.svelte +11 -11
- package/dist/primitives/ResizableSplitter.svelte +10 -10
- package/dist/primitives/TabbedPanel.svelte +19 -19
- package/dist/primitives/base.css +57 -57
- package/dist/primitives/widgets/ColorSwatch.svelte +15 -15
- package/dist/primitives/widgets/ColorSwatch.svelte.test.js +4 -4
- package/dist/primitives/widgets/Field.svelte +20 -20
- package/dist/primitives/widgets/FilePicker.svelte +13 -13
- package/dist/primitives/widgets/IconPicker.svelte +14 -14
- package/dist/primitives/widgets/IconToggleGroup.svelte +14 -14
- package/dist/primitives/widgets/NumberInput.svelte +17 -17
- package/dist/primitives/widgets/PickerList.svelte +17 -17
- package/dist/primitives/widgets/RangeSlider.svelte +16 -16
- package/dist/primitives/widgets/Segmented.svelte +16 -16
- package/dist/primitives/widgets/Select.svelte +14 -14
- package/dist/primitives/widgets/Select.svelte.test.js +2 -2
- package/dist/primitives/widgets/Slider.svelte +20 -20
- package/dist/primitives/widgets/SliderGroup.svelte +2 -2
- package/dist/primitives/widgets/Textarea.svelte +13 -13
- package/dist/primitives/widgets/_select-listbox.svelte +15 -15
- package/dist/projects/session-state.svelte.js +1 -1
- package/dist/projects-shard/DeleteProjectDialog.svelte +12 -12
- package/dist/projects-shard/ProjectManage.svelte +25 -25
- package/dist/projects-shard/ProjectsSection.svelte +12 -12
- package/dist/registry/installer.d.ts +2 -2
- package/dist/registry/installer.js +2 -2
- package/dist/registry/loader.js +1 -1
- package/dist/registry/permission-descriptions.js +2 -2
- package/dist/registry/permission-descriptions.test.js +1 -1
- package/dist/runtime/runVerb-shell.test.js +2 -2
- package/dist/runtime/runVerb.js +4 -4
- package/dist/runtime/runVerb.test.js +4 -4
- package/dist/satellite/SatelliteShell.svelte +2 -2
- package/dist/{shell → sh3}/views/KeysAndPeers.svelte +7 -7
- package/dist/sh3Api/headless.d.ts +4 -0
- package/dist/{shell-shard/shellApi.js → sh3Api/headless.js} +5 -5
- package/dist/{shell-shard/shellApi.svelte.test.js → sh3Api/headless.svelte.test.js} +11 -11
- package/dist/{shellApi → sh3Api}/window.js +2 -2
- package/dist/{shellApi → sh3Api}/window.test.js +1 -1
- package/dist/{shellRuntime.svelte.d.ts → sh3Runtime.svelte.d.ts} +11 -11
- package/dist/{shellRuntime.svelte.js → sh3Runtime.svelte.js} +10 -10
- package/dist/sh3Runtime.svelte.test.d.ts +1 -0
- package/dist/{shellRuntime.svelte.test.js → sh3Runtime.svelte.test.js} +10 -10
- package/dist/sh3core-shard/AppInfoView.svelte +19 -19
- package/dist/sh3core-shard/{ShellHome.svelte → Sh3Home.svelte} +82 -82
- package/dist/sh3core-shard/Sh3Home.svelte.d.ts +3 -0
- package/dist/sh3core-shard/{ShellTitle.svelte → Sh3Title.svelte} +11 -11
- package/dist/sh3core-shard/Sh3Title.svelte.d.ts +3 -0
- package/dist/sh3core-shard/appActions.js +3 -3
- package/dist/sh3core-shard/sh3coreShard.svelte.js +7 -7
- package/dist/shards/activate.svelte.d.ts +1 -1
- package/dist/shards/activate.svelte.js +6 -6
- package/dist/shards/types.d.ts +5 -5
- package/dist/shards/types.js +2 -2
- package/dist/shell-shard/CommandLine.svelte +7 -7
- package/dist/shell-shard/InputLine.svelte +3 -3
- package/dist/shell-shard/ScrollbackView.svelte +2 -2
- package/dist/shell-shard/Terminal.svelte +5 -5
- package/dist/shell-shard/Terminal.svelte.d.ts +2 -2
- package/dist/shell-shard/dispatch-custom.test.js +2 -2
- package/dist/shell-shard/dispatch-gating.test.js +2 -2
- package/dist/shell-shard/dispatch-invoke.test.js +2 -2
- package/dist/shell-shard/dispatch-to-terminal.js +1 -1
- package/dist/shell-shard/dispatch.d.ts +2 -2
- package/dist/shell-shard/dispatch.js +2 -2
- package/dist/shell-shard/entries/PromptEntry.svelte +4 -4
- package/dist/shell-shard/entries/StatusEntry.svelte +4 -4
- package/dist/shell-shard/entries/TextEntry.svelte +3 -3
- package/dist/shell-shard/locateSlot.test.js +9 -9
- package/dist/shell-shard/manifest.js +2 -2
- package/dist/shell-shard/registry.d.ts +2 -2
- package/dist/shell-shard/rich/AppCard.svelte +1 -1
- package/dist/shell-shard/rich/AppsTable.svelte +1 -1
- package/dist/shell-shard/rich/HelpTable.svelte +1 -1
- package/dist/shell-shard/rich/HistoryList.svelte +1 -1
- package/dist/shell-shard/rich/ViewsTable.svelte +1 -1
- package/dist/shell-shard/rich/ZoneTree.svelte +2 -2
- package/dist/shell-shard/shellShard.svelte.d.ts +1 -1
- package/dist/shell-shard/shellShard.svelte.js +8 -8
- package/dist/shell-shard/terminal-dispatch.test.js +2 -2
- package/dist/shell-shard/toolbar/Toolbar.svelte +4 -4
- package/dist/shell-shard/toolbar/Toolbar.svelte.d.ts +2 -2
- package/dist/shell-shard/toolbar/slots/BusySlot.svelte +2 -2
- package/dist/shell-shard/toolbar/slots/FocusLockSlot.svelte +2 -2
- package/dist/shell-shard/toolbar/slots/ModeSlot.svelte +1 -1
- package/dist/shell-shard/toolbar/slots/TargetShardSlot.svelte +1 -1
- package/dist/shell-shard/toolbar/slots.d.ts +3 -3
- package/dist/shell-shard/verbs/apps.js +3 -3
- package/dist/shell-shard/verbs/help.js +1 -1
- package/dist/shell-shard/verbs/help.svelte.test.js +1 -1
- package/dist/shell-shard/verbs/history.js +1 -1
- package/dist/shell-shard/verbs/mode.js +2 -2
- package/dist/shell-shard/verbs/mode.test.js +2 -2
- package/dist/shell-shard/verbs/shards.js +1 -1
- package/dist/shell-shard/verbs/views.js +8 -8
- package/dist/shell-shard/verbs/zones.js +2 -2
- package/dist/state/zones.svelte.d.ts +2 -2
- package/dist/state/zones.svelte.js +4 -4
- package/dist/theme.d.ts +3 -3
- package/dist/theme.js +5 -5
- package/dist/tokens.css +68 -68
- package/dist/verbs/types.d.ts +5 -5
- package/dist/verbs/types.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/Shell.svelte.d.ts +0 -5
- package/dist/sh3core-shard/ShellHome.svelte.d.ts +0 -3
- package/dist/sh3core-shard/ShellTitle.svelte.d.ts +0 -3
- package/dist/shell-shard/shellApi.d.ts +0 -4
- /package/dist/actions/{shellActions.test.d.ts → sh3Actions.test.d.ts} +0 -0
- /package/dist/{shell-shard/shellApi.svelte.test.d.ts → build.test.d.ts} +0 -0
- /package/dist/color/{shell-api.d.ts → sh3-api.d.ts} +0 -0
- /package/dist/conflicts/{shell-api.d.ts → sh3-api.d.ts} +0 -0
- /package/dist/{shell → sh3}/views/KeysAndPeers.svelte.d.ts +0 -0
- /package/dist/{shellApi/window.test.d.ts → sh3Api/headless.svelte.test.d.ts} +0 -0
- /package/dist/{shellApi → sh3Api}/window.d.ts +0 -0
- /package/dist/{shellRuntime.svelte.test.d.ts → sh3Api/window.test.d.ts} +0 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* unregisters view factories, drops from `activeShards`. The shard
|
|
17
17
|
* stays in `registeredShards` — it's still known, just not running.
|
|
18
18
|
*/
|
|
19
|
-
import {
|
|
19
|
+
import { sh3 } from '../sh3Runtime.svelte';
|
|
20
20
|
import { registerView, unregisterView, registerVerb as fwRegisterVerb, unregisterVerb as fwUnregisterVerb, listVerbsWithShard } from './registry';
|
|
21
21
|
import { runVerbProgrammatic } from '../runtime/runVerb';
|
|
22
22
|
import { createDocumentHandle, getTenantId, getDocumentBackend } from '../documents';
|
|
@@ -32,7 +32,7 @@ import { subscribe } from '../keys/revocation-bus.svelte';
|
|
|
32
32
|
import { register as contributionsRegister, list as contributionsList, listPoints as contributionsListPoints, onChange as contributionsOnChange, onAnyChange as contributionsOnAnyChange, } from '../contributions';
|
|
33
33
|
import { registerAction, listActions as listActionsFromRegistry } from '../actions/registry';
|
|
34
34
|
import { makeSelectionApi, clearSelectionForShard } from '../actions/selection.svelte';
|
|
35
|
-
import { openContextMenu as
|
|
35
|
+
import { openContextMenu as sh3OpenContextMenu, openPalette as sh3OpenPalette, dispatchActionProgrammatic, } from '../actions/listeners';
|
|
36
36
|
import { listActionsFromEntries } from '../actions/listActive';
|
|
37
37
|
import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
38
38
|
/**
|
|
@@ -40,7 +40,7 @@ import { getLiveDispatcherState } from '../actions/state.svelte';
|
|
|
40
40
|
* Populated once at boot by the glob-discovery loop in main.ts (through
|
|
41
41
|
* `registerShard`); a future runtime loader appends late without touching
|
|
42
42
|
* the rest of the framework. Callers that need a live list subscribe via
|
|
43
|
-
* Svelte reactivity (e.g.
|
|
43
|
+
* Svelte reactivity (e.g. sh3 home listing apps cross-references this
|
|
44
44
|
* map to show which shards they require).
|
|
45
45
|
*/
|
|
46
46
|
export const registeredShards = $state(new Map());
|
|
@@ -132,7 +132,7 @@ export async function activateShard(id, opts) {
|
|
|
132
132
|
},
|
|
133
133
|
};
|
|
134
134
|
const ctx = {
|
|
135
|
-
state: (schema) =>
|
|
135
|
+
state: (schema) => sh3.state(id, schema),
|
|
136
136
|
registerView: (viewId, factory) => {
|
|
137
137
|
registerView(viewId, factory, shard.manifest.id);
|
|
138
138
|
entry.viewIds.add(viewId);
|
|
@@ -200,8 +200,8 @@ export async function activateShard(id, opts) {
|
|
|
200
200
|
return dispose;
|
|
201
201
|
},
|
|
202
202
|
selection: makeSelectionApi(id),
|
|
203
|
-
openContextMenu(opts) {
|
|
204
|
-
openPalette(opts) {
|
|
203
|
+
openContextMenu(opts) { sh3OpenContextMenu(opts); },
|
|
204
|
+
openPalette(opts) { sh3OpenPalette(opts); },
|
|
205
205
|
},
|
|
206
206
|
listVerbs(opts) {
|
|
207
207
|
const all = listVerbsWithShard();
|
package/dist/shards/types.d.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface MountContext {
|
|
|
52
52
|
label: string;
|
|
53
53
|
/**
|
|
54
54
|
* Caller-supplied instance data. Present when the mount was triggered with
|
|
55
|
-
* metadata (e.g. `
|
|
55
|
+
* metadata (e.g. `sh3.float.open(viewId, { meta: { ... } })`).
|
|
56
56
|
* Not persisted with the layout — ephemeral per mount.
|
|
57
57
|
*/
|
|
58
58
|
meta?: Record<string, unknown>;
|
|
@@ -181,15 +181,15 @@ export interface ShardContext {
|
|
|
181
181
|
/**
|
|
182
182
|
* Register a view factory for a view id declared in the shard manifest.
|
|
183
183
|
* Must be called for every id listed in `manifest.views` during `activate`.
|
|
184
|
-
* The shard id is injected automatically by the
|
|
184
|
+
* The shard id is injected automatically by the sh3 — shards do not need to pass it.
|
|
185
185
|
*
|
|
186
186
|
* @param viewId - Must match an entry in `manifest.views`.
|
|
187
187
|
* @param factory - The adapter that mounts the view into a container element.
|
|
188
188
|
*/
|
|
189
189
|
registerView(viewId: string, factory: ViewFactory): void;
|
|
190
190
|
/**
|
|
191
|
-
* Register a verb that users can invoke from the
|
|
192
|
-
* The verb name is auto-prefixed with `shardId:` for non-
|
|
191
|
+
* Register a verb that users can invoke from the sh3 terminal.
|
|
192
|
+
* The verb name is auto-prefixed with `shardId:` for non-sh3 shards.
|
|
193
193
|
* Automatically unregistered when the shard deactivates.
|
|
194
194
|
*
|
|
195
195
|
* @param verb - The verb definition (name, summary, run function).
|
|
@@ -333,7 +333,7 @@ export interface ShardContext {
|
|
|
333
333
|
/**
|
|
334
334
|
* A shard module. Shards are the fundamental unit of contribution in SH3.
|
|
335
335
|
* Each shard activates once, receives a `ShardContext`, and registers views
|
|
336
|
-
* and other capabilities that the
|
|
336
|
+
* and other capabilities that the sh3 and apps can use. Shard lifecycle is
|
|
337
337
|
* separate from view lifecycle — a shard may be active with no views visible.
|
|
338
338
|
*/
|
|
339
339
|
export interface Shard {
|
package/dist/shards/types.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* Shard contract — minimum viable draft for phase 4.
|
|
3
3
|
*
|
|
4
4
|
* A shard is a self-contained module that contributes capabilities (views,
|
|
5
|
-
* commands, services, …) to the
|
|
5
|
+
* commands, services, …) to the sh3. See docs/design/shards.md for the
|
|
6
6
|
* full design; phase 4 implements only the pieces needed to mount a view
|
|
7
7
|
* into a layout slot:
|
|
8
8
|
*
|
|
9
9
|
* - A shard declares a manifest.
|
|
10
10
|
* - `activate(ctx)` runs once, receives a ShardContext, and registers
|
|
11
|
-
* whichever contributions it wants the
|
|
11
|
+
* whichever contributions it wants the sh3 to know about.
|
|
12
12
|
* - A ViewFactory knows how to mount a view into a raw HTMLElement and
|
|
13
13
|
* return a handle the framework uses to unmount / notify of resizes.
|
|
14
14
|
*
|
|
@@ -92,16 +92,16 @@
|
|
|
92
92
|
gap: 6px;
|
|
93
93
|
padding: 4px 8px;
|
|
94
94
|
border: 1px solid transparent;
|
|
95
|
-
border-radius: var(--
|
|
96
|
-
font-family: var(--
|
|
97
|
-
font-size: var(--
|
|
95
|
+
border-radius: var(--sh3-radius);
|
|
96
|
+
font-family: var(--sh3-font-mono, monospace);
|
|
97
|
+
font-size: var(--sh3-font-size, 13px);
|
|
98
98
|
line-height: 1.4;
|
|
99
|
-
transition: border-color var(--
|
|
100
|
-
box-shadow var(--
|
|
99
|
+
transition: border-color var(--sh3-motion-fast) var(--sh3-ease-standard),
|
|
100
|
+
box-shadow var(--sh3-motion-fast) var(--sh3-ease-standard);
|
|
101
101
|
}
|
|
102
102
|
.sh3-cmdline:focus-within {
|
|
103
|
-
border-color: var(--
|
|
104
|
-
box-shadow: var(--
|
|
103
|
+
border-color: var(--sh3-input-border-focus);
|
|
104
|
+
box-shadow: var(--sh3-focus-ring);
|
|
105
105
|
}
|
|
106
106
|
.sh3-cmdline__prefix {
|
|
107
107
|
flex-shrink: 0;
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
|
|
108
108
|
<style>
|
|
109
109
|
.shell-input-row {
|
|
110
|
-
border-top: 1px solid var(--
|
|
110
|
+
border-top: 1px solid var(--sh3-border, #333);
|
|
111
111
|
}
|
|
112
|
-
.shell-input-cwd { color: var(--
|
|
113
|
-
.shell-input-arrow { color: var(--
|
|
112
|
+
.shell-input-cwd { color: var(--sh3-fg-muted, #888); }
|
|
113
|
+
.shell-input-arrow { color: var(--sh3-accent, #6cf); margin-left: 6px; }
|
|
114
114
|
</style>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import ScrollbackView from './ScrollbackView.svelte';
|
|
8
8
|
import InputLine from './InputLine.svelte';
|
|
9
9
|
import { SessionClient } from './session-client.svelte';
|
|
10
|
-
import { VerbRegistry, type
|
|
10
|
+
import { VerbRegistry, type Sh3Api } from './registry';
|
|
11
11
|
import type { ServerMessage } from './protocol';
|
|
12
12
|
import { TenantFsClient } from './tenant-fs-client';
|
|
13
13
|
import { ShellModeRegistry } from './modes/registry';
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { makeDispatchToTerminal } from './dispatch-to-terminal';
|
|
31
31
|
|
|
32
32
|
interface Props {
|
|
33
|
-
shell:
|
|
33
|
+
shell: Sh3Api;
|
|
34
34
|
wsUrl: string;
|
|
35
35
|
userId: string;
|
|
36
36
|
role: ShellRole;
|
|
@@ -196,7 +196,7 @@
|
|
|
196
196
|
// and the setMode closure, so the wrapper happens here. The shell prop
|
|
197
197
|
// is stable for the view's lifetime, so capturing its initial value via
|
|
198
198
|
// untrack is intentional.
|
|
199
|
-
const shellWithModes:
|
|
199
|
+
const shellWithModes: Sh3Api = untrack(() => ({
|
|
200
200
|
...shell,
|
|
201
201
|
setMode: (id: string) => {
|
|
202
202
|
const next = visibleModes.find((m) => m.id === id);
|
|
@@ -438,7 +438,7 @@
|
|
|
438
438
|
display: flex;
|
|
439
439
|
flex-direction: column;
|
|
440
440
|
height: 100%;
|
|
441
|
-
background: var(--
|
|
442
|
-
color: var(--
|
|
441
|
+
background: var(--sh3-bg, #111);
|
|
442
|
+
color: var(--sh3-fg, #ddd);
|
|
443
443
|
}
|
|
444
444
|
</style>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Sh3Api } from './registry';
|
|
2
2
|
import type { ShellRole } from './modes/types';
|
|
3
3
|
import type { ContributionsApi } from '../contributions/types';
|
|
4
4
|
interface Props {
|
|
5
|
-
shell:
|
|
5
|
+
shell: Sh3Api;
|
|
6
6
|
wsUrl: string;
|
|
7
7
|
userId: string;
|
|
8
8
|
role: ShellRole;
|
|
@@ -10,7 +10,7 @@ function makeStubDeps(mode, customMode) {
|
|
|
10
10
|
send: () => { },
|
|
11
11
|
cwd: '/',
|
|
12
12
|
};
|
|
13
|
-
const
|
|
13
|
+
const sh3 = {};
|
|
14
14
|
const fs = {};
|
|
15
15
|
const resolver = {
|
|
16
16
|
resolve: (line) => ({ kind: 'forward', line }),
|
|
@@ -22,7 +22,7 @@ function makeStubDeps(mode, customMode) {
|
|
|
22
22
|
resolver,
|
|
23
23
|
buffer: () => buffer,
|
|
24
24
|
session,
|
|
25
|
-
|
|
25
|
+
sh3,
|
|
26
26
|
fs,
|
|
27
27
|
cwd: () => '/',
|
|
28
28
|
busy: () => () => { },
|
|
@@ -17,7 +17,7 @@ function scaffold(mode) {
|
|
|
17
17
|
const buffer = { scrollback, history };
|
|
18
18
|
const session = { history: { push: vi.fn() }, send: (m) => sent.push(m), cwd: '/', connected: true, connect: vi.fn() };
|
|
19
19
|
const fs = {};
|
|
20
|
-
const
|
|
20
|
+
const sh3 = {};
|
|
21
21
|
// Real-shape resolver to exercise the new globalOnly path.
|
|
22
22
|
const resolver = {
|
|
23
23
|
resolve: (line, opts = {}) => {
|
|
@@ -36,7 +36,7 @@ function scaffold(mode) {
|
|
|
36
36
|
resolver,
|
|
37
37
|
buffer: () => buffer,
|
|
38
38
|
session,
|
|
39
|
-
|
|
39
|
+
sh3,
|
|
40
40
|
fs,
|
|
41
41
|
cwd: () => '/',
|
|
42
42
|
busy: () => () => { },
|
|
@@ -22,7 +22,7 @@ function scaffold(opts) {
|
|
|
22
22
|
connect: connectSpy,
|
|
23
23
|
};
|
|
24
24
|
const fs = {};
|
|
25
|
-
const
|
|
25
|
+
const sh3 = {};
|
|
26
26
|
const resolver = {
|
|
27
27
|
resolve: (line, _opts = {}) => {
|
|
28
28
|
const head = line.trim().split(/\s+/)[0];
|
|
@@ -38,7 +38,7 @@ function scaffold(opts) {
|
|
|
38
38
|
resolver,
|
|
39
39
|
buffer: () => buffer,
|
|
40
40
|
session,
|
|
41
|
-
|
|
41
|
+
sh3,
|
|
42
42
|
fs,
|
|
43
43
|
cwd: () => '/',
|
|
44
44
|
busy: () => () => { },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* makeDispatchToTerminal — factory returning the `shell.dispatchToTerminal`
|
|
3
3
|
* function. Two flavors:
|
|
4
|
-
* - headless: always returns 'no-active-context'. Used by
|
|
4
|
+
* - headless: always returns 'no-active-context'. Used by makeSh3ApiHeadless
|
|
5
5
|
* so verbs run via runVerbProgrammatic (and node-only tests) get a clean
|
|
6
6
|
* failure instead of hitting the live registry.
|
|
7
7
|
* - live: consults the module-scoped terminal-registry and routes through the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { VerbRegistry,
|
|
1
|
+
import type { VerbRegistry, Sh3Api } from './registry';
|
|
2
2
|
import type { SessionClient } from './session-client.svelte';
|
|
3
3
|
import type { TenantFsClient } from './tenant-fs-client';
|
|
4
4
|
import type { ModeBuffer } from './mode-buffer.svelte';
|
|
@@ -16,7 +16,7 @@ export interface DispatchDeps {
|
|
|
16
16
|
*/
|
|
17
17
|
buffer: () => ModeBuffer;
|
|
18
18
|
session: SessionClient;
|
|
19
|
-
|
|
19
|
+
sh3: Sh3Api;
|
|
20
20
|
fs: TenantFsClient;
|
|
21
21
|
cwd: () => string;
|
|
22
22
|
/**
|
|
@@ -33,7 +33,7 @@ export function makeDispatch(deps) {
|
|
|
33
33
|
throw new Error(`invoke: unknown sh3 verb '${head}'`);
|
|
34
34
|
}
|
|
35
35
|
await resolution.verb.run({
|
|
36
|
-
|
|
36
|
+
sh3: deps.sh3,
|
|
37
37
|
scrollback: deps.buffer().scrollback,
|
|
38
38
|
session: deps.session,
|
|
39
39
|
cwd: deps.cwd(),
|
|
@@ -93,7 +93,7 @@ export function makeDispatch(deps) {
|
|
|
93
93
|
deps.buffer().scrollback.push({ kind: 'prompt', cwd: deps.cwd(), line, ts: Date.now() });
|
|
94
94
|
try {
|
|
95
95
|
await resolution.verb.run({
|
|
96
|
-
|
|
96
|
+
sh3: deps.sh3,
|
|
97
97
|
scrollback: deps.buffer().scrollback,
|
|
98
98
|
session: deps.session,
|
|
99
99
|
cwd: deps.cwd(),
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
<style>
|
|
16
16
|
.shell-prompt {
|
|
17
17
|
padding: 4px 8px 0 8px;
|
|
18
|
-
font-family: var(--
|
|
18
|
+
font-family: var(--sh3-font-mono, monospace);
|
|
19
19
|
display: flex;
|
|
20
20
|
gap: 8px;
|
|
21
21
|
}
|
|
22
|
-
.shell-prompt-cwd { color: var(--
|
|
23
|
-
.shell-prompt-arrow { color: var(--
|
|
24
|
-
.shell-prompt-line { color: var(--
|
|
22
|
+
.shell-prompt-cwd { color: var(--sh3-fg-muted, #888); }
|
|
23
|
+
.shell-prompt-arrow { color: var(--sh3-accent, #6cf); }
|
|
24
|
+
.shell-prompt-line { color: var(--sh3-fg, #ddd); }
|
|
25
25
|
</style>
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
<style>
|
|
14
14
|
.shell-status {
|
|
15
15
|
padding: 2px 8px;
|
|
16
|
-
font-family: var(--
|
|
16
|
+
font-family: var(--sh3-font-mono, monospace);
|
|
17
17
|
font-style: italic;
|
|
18
18
|
white-space: pre-wrap;
|
|
19
19
|
word-break: break-word;
|
|
20
20
|
}
|
|
21
|
-
.shell-status.info { color: var(--
|
|
22
|
-
.shell-status.warn { color: var(--
|
|
23
|
-
.shell-status.error { color: var(--
|
|
21
|
+
.shell-status.info { color: var(--sh3-fg-muted, #888); }
|
|
22
|
+
.shell-status.warn { color: var(--sh3-fg-warn, #fc6); }
|
|
23
|
+
.shell-status.error { color: var(--sh3-fg-error, #f88); }
|
|
24
24
|
</style>
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
.shell-text {
|
|
17
17
|
margin: 0;
|
|
18
18
|
padding: 0 8px;
|
|
19
|
-
font-family: var(--
|
|
19
|
+
font-family: var(--sh3-font-mono, monospace);
|
|
20
20
|
white-space: pre-wrap;
|
|
21
21
|
word-break: break-word;
|
|
22
|
-
color: var(--
|
|
22
|
+
color: var(--sh3-fg, #ddd);
|
|
23
23
|
}
|
|
24
|
-
.shell-text.stderr { color: var(--
|
|
24
|
+
.shell-text.stderr { color: var(--sh3-fg-error, #f88); }
|
|
25
25
|
</style>
|
|
@@ -5,8 +5,8 @@ import { registerApp } from '../apps/registry.svelte';
|
|
|
5
5
|
import { launchApp } from '../apps/lifecycle';
|
|
6
6
|
import { floatManager, bindFloatStore } from '../overlays/float';
|
|
7
7
|
import { layoutStore } from '../layout/store.svelte';
|
|
8
|
-
import {
|
|
9
|
-
describe('
|
|
8
|
+
import { makeSh3ApiForTest } from './shellShard.svelte';
|
|
9
|
+
describe('Sh3Api.locateSlot', () => {
|
|
10
10
|
beforeEach(resetFramework);
|
|
11
11
|
it('returns docked for a slot in the docked tree', async () => {
|
|
12
12
|
registerApp(makeApp({
|
|
@@ -19,7 +19,7 @@ describe('ShellApi.locateSlot', () => {
|
|
|
19
19
|
],
|
|
20
20
|
}));
|
|
21
21
|
await launchApp('test-app-docked');
|
|
22
|
-
const shell =
|
|
22
|
+
const shell = makeSh3ApiForTest();
|
|
23
23
|
expect(shell.locateSlot('dock-x')).toEqual({ kind: 'docked' });
|
|
24
24
|
});
|
|
25
25
|
it('returns float for a slot in a float', async () => {
|
|
@@ -34,7 +34,7 @@ describe('ShellApi.locateSlot', () => {
|
|
|
34
34
|
],
|
|
35
35
|
}));
|
|
36
36
|
await launchApp('test-app-float');
|
|
37
|
-
//
|
|
37
|
+
// Sh3.svelte normally binds the float manager to the active tree's
|
|
38
38
|
// floats array during boot. In tests we bind it manually so
|
|
39
39
|
// floatManager.open() writes into the tree that locateSlot walks.
|
|
40
40
|
bindFloatStore(layoutStore.floats, () => ({ w: 1024, h: 768 }));
|
|
@@ -44,7 +44,7 @@ describe('ShellApi.locateSlot', () => {
|
|
|
44
44
|
const tabs = openedFloat.content.type === 'tabs' ? openedFloat.content : null;
|
|
45
45
|
const floatSlotId = (_a = tabs === null || tabs === void 0 ? void 0 : tabs.tabs[0]) === null || _a === void 0 ? void 0 : _a.slotId;
|
|
46
46
|
expect(floatSlotId).toBeTruthy();
|
|
47
|
-
const shell =
|
|
47
|
+
const shell = makeSh3ApiForTest();
|
|
48
48
|
expect(shell.locateSlot(floatSlotId)).toEqual({ kind: 'float', floatId });
|
|
49
49
|
});
|
|
50
50
|
it('returns null for an absent slot', async () => {
|
|
@@ -58,12 +58,12 @@ describe('ShellApi.locateSlot', () => {
|
|
|
58
58
|
],
|
|
59
59
|
}));
|
|
60
60
|
await launchApp('test-app-absent');
|
|
61
|
-
const shell =
|
|
61
|
+
const shell = makeSh3ApiForTest();
|
|
62
62
|
expect(shell.locateSlot('nonexistent-slot')).toBeNull();
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
65
|
import { popoutView, inspectActiveLayout } from '../layout/inspection';
|
|
66
|
-
describe('
|
|
66
|
+
describe('Sh3Api.locateSlot — round-trip', () => {
|
|
67
67
|
beforeEach(resetFramework);
|
|
68
68
|
it('tracks the docked → popout → float transition', async () => {
|
|
69
69
|
registerApp(makeApp({
|
|
@@ -79,9 +79,9 @@ describe('ShellApi.locateSlot — round-trip', () => {
|
|
|
79
79
|
],
|
|
80
80
|
}));
|
|
81
81
|
await launchApp('test-app-rt');
|
|
82
|
-
//
|
|
82
|
+
// Sh3.svelte would bind this at boot; in tests we do it ourselves.
|
|
83
83
|
bindFloatStore(layoutStore.floats, () => ({ w: 1024, h: 768 }));
|
|
84
|
-
const shell =
|
|
84
|
+
const shell = makeSh3ApiForTest();
|
|
85
85
|
// Starts docked.
|
|
86
86
|
expect(shell.locateSlot('rt-1')).toEqual({ kind: 'docked' });
|
|
87
87
|
// Popout removes the original docked tab and opens a float with a
|
|
@@ -2,9 +2,9 @@ import { VERSION } from '../version';
|
|
|
2
2
|
import { PERMISSION_STATE_MANAGE } from '../state/types';
|
|
3
3
|
export const manifest = {
|
|
4
4
|
id: 'shell',
|
|
5
|
-
label: '
|
|
5
|
+
label: 'Sh3',
|
|
6
6
|
version: VERSION,
|
|
7
|
-
views: [{ id: 'shell:terminal', label: '
|
|
7
|
+
views: [{ id: 'shell:terminal', label: 'Sh3', standalone: true }],
|
|
8
8
|
// serverBundle intentionally omitted — this shard is a framework built-in
|
|
9
9
|
// and is statically mounted at sh3-server boot. The existing contract in
|
|
10
10
|
// sh3-core/src/shards/types.ts documents that framework-shipped shards do
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Verb, VerbContext, Resolution,
|
|
2
|
-
export type { Verb, VerbContext, Resolution,
|
|
1
|
+
import type { Verb, VerbContext, Resolution, Sh3Api } from '../verbs/types';
|
|
2
|
+
export type { Verb, VerbContext, Resolution, Sh3Api };
|
|
3
3
|
export declare class VerbRegistry {
|
|
4
4
|
list(): Verb[];
|
|
5
5
|
get(name: string): Verb | undefined;
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
<style>
|
|
25
25
|
.shell-rich-apps table { border-collapse: collapse; width: 100%; }
|
|
26
26
|
.shell-rich-apps th, .shell-rich-apps td { padding: 2px 8px; text-align: left; }
|
|
27
|
-
.shell-rich-apps button { background: none; border: 0; color: var(--
|
|
27
|
+
.shell-rich-apps button { background: none; border: 0; color: var(--sh3-link, #6cf); cursor: pointer; padding: 0; font: inherit; }
|
|
28
28
|
.shell-rich-apps button:hover { text-decoration: underline; }
|
|
29
29
|
</style>
|
|
@@ -24,6 +24,6 @@
|
|
|
24
24
|
<style>
|
|
25
25
|
.shell-rich-help table { border-collapse: collapse; width: 100%; }
|
|
26
26
|
.shell-rich-help th, .shell-rich-help td { padding: 2px 8px; text-align: left; }
|
|
27
|
-
.shell-rich-help button { background: none; border: 0; color: var(--
|
|
27
|
+
.shell-rich-help button { background: none; border: 0; color: var(--sh3-link, #6cf); cursor: pointer; padding: 0; font: inherit; }
|
|
28
28
|
.shell-rich-help button:hover { text-decoration: underline; }
|
|
29
29
|
</style>
|
|
@@ -26,6 +26,6 @@
|
|
|
26
26
|
<style>
|
|
27
27
|
.shell-rich-views table { border-collapse: collapse; width: 100%; }
|
|
28
28
|
.shell-rich-views th, .shell-rich-views td { padding: 2px 8px; text-align: left; }
|
|
29
|
-
.shell-rich-views button { background: none; border: 0; color: var(--
|
|
29
|
+
.shell-rich-views button { background: none; border: 0; color: var(--sh3-link, #6cf); cursor: pointer; padding: 0; font: inherit; }
|
|
30
30
|
.shell-rich-views button:hover { text-decoration: underline; }
|
|
31
31
|
</style>
|
|
@@ -12,25 +12,25 @@
|
|
|
12
12
|
* autostart() is defined so the shard activates at boot without requiring
|
|
13
13
|
* a dedicated app to launch it first, matching the __sh3core__ pattern.
|
|
14
14
|
*
|
|
15
|
-
* The headless
|
|
16
|
-
* (node-only test project, runtime/runVerb) can build a
|
|
17
|
-
* pulling in Terminal.svelte.
|
|
15
|
+
* The headless Sh3Api factory lives in ../sh3Api/headless.ts so non-DOM
|
|
16
|
+
* callers (node-only test project, runtime/runVerb) can build a Sh3Api
|
|
17
|
+
* without pulling in Terminal.svelte.
|
|
18
18
|
*/
|
|
19
19
|
import { mount, unmount } from 'svelte';
|
|
20
20
|
import { manifest } from './manifest';
|
|
21
21
|
import Terminal from './Terminal.svelte';
|
|
22
22
|
import { registerV1Verbs } from './verbs';
|
|
23
|
-
import {
|
|
23
|
+
import { makeSh3ApiHeadless } from '../sh3Api/headless';
|
|
24
24
|
import { focusView } from '../layout/inspection';
|
|
25
25
|
import { floatManager } from '../overlays/float';
|
|
26
26
|
import { getUser, isAdmin } from '../auth/index';
|
|
27
27
|
import { __bindZone, __unbindZone } from './buffer-zone-state.svelte';
|
|
28
|
-
export {
|
|
28
|
+
export { makeSh3ApiHeadless, makeSh3ApiForTest } from '../sh3Api/headless';
|
|
29
29
|
export const shellShard = {
|
|
30
30
|
manifest,
|
|
31
31
|
activate(ctx) {
|
|
32
32
|
registerV1Verbs(ctx);
|
|
33
|
-
const shell =
|
|
33
|
+
const shell = makeSh3ApiHeadless(ctx.zones);
|
|
34
34
|
// Bind the shell-shard's workspace zone — backs scrollback persistence
|
|
35
35
|
// (SH8). BufferStore reads/writes through this proxy.
|
|
36
36
|
const zone = ctx.state({
|
|
@@ -39,7 +39,7 @@ export const shellShard = {
|
|
|
39
39
|
__bindZone(zone);
|
|
40
40
|
// The AZERTY `²` key (top-left on FR keyboards, below Escape) opens the
|
|
41
41
|
// terminal view — focusing it if already mounted, floating it otherwise.
|
|
42
|
-
// Migrated from
|
|
42
|
+
// Migrated from Sh3.svelte's inline keydown handler as proof-of-concept
|
|
43
43
|
// for the Actions framework (Task 23 / DF1). Registered here because this
|
|
44
44
|
// shard owns the terminal view and is already admin-gated.
|
|
45
45
|
ctx.actions.register({
|
|
@@ -50,7 +50,7 @@ export const shellShard = {
|
|
|
50
50
|
allowInInputs: false,
|
|
51
51
|
run() {
|
|
52
52
|
if (!focusView('shell:terminal'))
|
|
53
|
-
floatManager.open('shell:terminal', { title: '
|
|
53
|
+
floatManager.open('shell:terminal', { title: 'Sh3' });
|
|
54
54
|
},
|
|
55
55
|
});
|
|
56
56
|
const factory = {
|
|
@@ -15,7 +15,7 @@ function scaffold(modeId) {
|
|
|
15
15
|
cwd: '/',
|
|
16
16
|
};
|
|
17
17
|
const fs = {};
|
|
18
|
-
const
|
|
18
|
+
const sh3 = {};
|
|
19
19
|
const resolver = {
|
|
20
20
|
resolve: (line, opts = {}) => {
|
|
21
21
|
// The test only exercises 'pwd' (sh3-domain) and unknown lines.
|
|
@@ -32,7 +32,7 @@ function scaffold(modeId) {
|
|
|
32
32
|
resolver,
|
|
33
33
|
buffer: () => buffer,
|
|
34
34
|
session,
|
|
35
|
-
|
|
35
|
+
sh3,
|
|
36
36
|
fs,
|
|
37
37
|
cwd: () => '/',
|
|
38
38
|
busy: () => () => { },
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { ToolbarSlotRegistry,
|
|
2
|
+
import type { ToolbarSlotRegistry, Sh3SlotCtx } from './slots';
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
5
|
registry: ToolbarSlotRegistry;
|
|
6
|
-
ctx:
|
|
6
|
+
ctx: Sh3SlotCtx;
|
|
7
7
|
slotProps?: Record<string, Record<string, unknown>>;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
align-items: center;
|
|
28
28
|
gap: 6px;
|
|
29
29
|
padding: 4px 6px;
|
|
30
|
-
background: var(--
|
|
31
|
-
border-bottom: 1px solid var(--
|
|
30
|
+
background: var(--sh3-bg-elevated, var(--sh3-bg, #1a1a1a));
|
|
31
|
+
border-bottom: 1px solid var(--sh3-border, #333);
|
|
32
32
|
flex-shrink: 0;
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { ToolbarSlotRegistry,
|
|
1
|
+
import type { ToolbarSlotRegistry, Sh3SlotCtx } from './slots';
|
|
2
2
|
interface Props {
|
|
3
3
|
registry: ToolbarSlotRegistry;
|
|
4
|
-
ctx:
|
|
4
|
+
ctx: Sh3SlotCtx;
|
|
5
5
|
slotProps?: Record<string, Record<string, unknown>>;
|
|
6
6
|
}
|
|
7
7
|
declare const Toolbar: import("svelte").Component<Props, {}, "">;
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
display: inline-flex;
|
|
19
19
|
align-items: center;
|
|
20
20
|
gap: 6px;
|
|
21
|
-
color: var(--
|
|
21
|
+
color: var(--sh3-fg-muted, #aaa);
|
|
22
22
|
font-size: 0.85em;
|
|
23
23
|
}
|
|
24
24
|
.spinner {
|
|
25
25
|
width: 12px;
|
|
26
26
|
height: 12px;
|
|
27
|
-
border: 2px solid var(--
|
|
27
|
+
border: 2px solid var(--sh3-fg-muted, #aaa);
|
|
28
28
|
border-top-color: transparent;
|
|
29
29
|
border-radius: 50%;
|
|
30
30
|
animation: busy-spin 0.8s linear infinite;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
<style>
|
|
15
15
|
.focus-lock-btn {
|
|
16
16
|
background: none;
|
|
17
|
-
border: 1px solid var(--
|
|
17
|
+
border: 1px solid var(--sh3-border, #444);
|
|
18
18
|
border-radius: 3px;
|
|
19
19
|
cursor: pointer;
|
|
20
20
|
padding: 2px 5px;
|
|
@@ -23,6 +23,6 @@
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
.focus-lock-btn:hover {
|
|
26
|
-
background: var(--
|
|
26
|
+
background: var(--sh3-hover, #222);
|
|
27
27
|
}
|
|
28
28
|
</style>
|