diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..91e9a66 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "web", + "runtimeExecutable": "npm", + "runtimeArgs": ["--prefix", "web", "run", "dev"], + "port": 5173 + } + ] +} diff --git a/api/openapi.yaml b/api/openapi.yaml index a97832d..cfd9c81 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -312,6 +312,17 @@ paths: type: string style: form explode: true + - name: include + in: query + schema: + type: array + items: + type: string + enum: + - status + style: form + explode: true + description: include=status joins entity_status and populates GraphView.health responses: '200': description: Graph view @@ -1696,6 +1707,22 @@ paths: $ref: '#/components/schemas/HealthSummary' default: $ref: '#/components/responses/Problem' + /dashboard/summary: + get: + tags: + - observability + operationId: getDashboardSummary + summary: One-round-trip overview for the control room home page + x-required-scope: viewer + responses: + '200': + description: Dashboard summary + content: + application/json: + schema: + $ref: '#/components/schemas/DashboardSummary' + default: + $ref: '#/components/responses/Problem' /export: get: tags: @@ -1983,6 +2010,16 @@ components: truncated: type: boolean description: True if node cap was hit + health: + type: object + description: entity id -> health, present when include=status was requested + additionalProperties: + type: string + enum: + - healthy + - degraded + - down + - unknown EntityType: type: object required: @@ -3024,6 +3061,69 @@ components: type: string format: date-time nullable: true + DashboardSummary: + type: object + required: + - entities_by_type + - entities_by_state + - health + - signals_by_severity + - approvals_pending + - executions_by_state + - event_rate + properties: + entities_by_type: + type: object + description: entity counts keyed by type + additionalProperties: + type: integer + entities_by_state: + type: object + description: entity counts keyed by state + additionalProperties: + type: integer + health: + type: object + required: + - healthy + - degraded + - down + - unknown + properties: + healthy: + type: integer + degraded: + type: integer + down: + type: integer + unknown: + type: integer + signals_by_severity: + type: object + description: open (non-resolved) signal counts keyed by severity + additionalProperties: + type: integer + approvals_pending: + type: integer + executions_by_state: + type: object + description: execution counts keyed by state, last 24h + additionalProperties: + type: integer + event_rate: + type: array + description: event counts bucketed by 5-minute interval, most recent last + items: + type: object + required: + - bucket + - count + properties: + bucket: + type: string + format: date-time + count: + type: integer EnrollRequest: type: object required: diff --git a/internal/db/sqlcgen/models.go b/internal/db/sqlcgen/models.go index 6877597..4b8df5d 100644 --- a/internal/db/sqlcgen/models.go +++ b/internal/db/sqlcgen/models.go @@ -26,6 +26,22 @@ type AgentActivity struct { CorrelationID *string } +type AgentMessage struct { + ID uuid.UUID + SessionID uuid.UUID + Role string + Content []byte + CreatedAt time.Time +} + +type AgentSession struct { + ID uuid.UUID + Title string + Actor string + CreatedAt time.Time + LastActiveAt time.Time +} + type Approval struct { EntityID uuid.UUID SubjectEntityID *uuid.UUID diff --git a/internal/httpapi/dashboard.go b/internal/httpapi/dashboard.go new file mode 100644 index 0000000..e284705 --- /dev/null +++ b/internal/httpapi/dashboard.go @@ -0,0 +1,160 @@ +package httpapi + +import ( + "context" + "time" + + "github.com/dtoro/oikos/internal/httpapi/gen" +) + +// GetDashboardSummary returns one round-trip overview for the control room +// home page: entity counts, health rollup, open signals, pending approvals, +// executions in the last 24h, and an event-rate sparkline. +func (s *Server) GetDashboardSummary(ctx context.Context, req gen.GetDashboardSummaryRequestObject) (gen.GetDashboardSummaryResponseObject, error) { + resp := gen.DashboardSummary{ + EntitiesByType: map[string]int{}, + EntitiesByState: map[string]int{}, + SignalsBySeverity: map[string]int{}, + ExecutionsByState: map[string]int{}, + } + + rows, err := s.pool.Query(ctx, `SELECT type, count(*) FROM entities GROUP BY type`) + if err != nil { + return nil, err + } + for rows.Next() { + var typ string + var n int + if err := rows.Scan(&typ, &n); err != nil { + rows.Close() + return nil, err + } + resp.EntitiesByType[typ] = n + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + rows, err = s.pool.Query(ctx, `SELECT coalesce(state, 'unknown'), count(*) FROM entities GROUP BY state`) + if err != nil { + return nil, err + } + for rows.Next() { + var state string + var n int + if err := rows.Scan(&state, &n); err != nil { + rows.Close() + return nil, err + } + resp.EntitiesByState[state] = n + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + rows, err = s.pool.Query(ctx, `SELECT health, count(*) FROM entity_status GROUP BY health`) + if err != nil { + return nil, err + } + for rows.Next() { + var health string + var n int + if err := rows.Scan(&health, &n); err != nil { + rows.Close() + return nil, err + } + switch health { + case "healthy": + resp.Health.Healthy = n + case "degraded": + resp.Health.Degraded = n + case "down": + resp.Health.Down = n + default: + resp.Health.Unknown = n + } + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + rows, err = s.pool.Query(ctx, ` + SELECT severity, count(*) FROM signals + WHERE state NOT IN ('resolved', 'failed') + GROUP BY severity`) + if err != nil { + return nil, err + } + for rows.Next() { + var severity string + var n int + if err := rows.Scan(&severity, &n); err != nil { + rows.Close() + return nil, err + } + resp.SignalsBySeverity[severity] = n + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + if err := s.pool.QueryRow(ctx, + `SELECT count(*) FROM approvals WHERE status = 'pending'`, + ).Scan(&resp.ApprovalsPending); err != nil { + return nil, err + } + + rows, err = s.pool.Query(ctx, ` + SELECT status, count(*) FROM executions + WHERE created_at > now() - interval '24 hours' + GROUP BY status`) + if err != nil { + return nil, err + } + for rows.Next() { + var status string + var n int + if err := rows.Scan(&status, &n); err != nil { + rows.Close() + return nil, err + } + resp.ExecutionsByState[status] = n + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + rows, err = s.pool.Query(ctx, ` + SELECT date_trunc('hour', ts) + (extract(minute FROM ts)::int / 5) * interval '5 minutes' AS bucket, + count(*) + FROM events + WHERE ts > now() - interval '6 hours' + GROUP BY bucket + ORDER BY bucket`) + if err != nil { + return nil, err + } + for rows.Next() { + var bucket time.Time + var n int + if err := rows.Scan(&bucket, &n); err != nil { + rows.Close() + return nil, err + } + resp.EventRate = append(resp.EventRate, struct { + Bucket time.Time `json:"bucket"` + Count int `json:"count"` + }{Bucket: bucket, Count: n}) + } + rows.Close() + if err := rows.Err(); err != nil { + return nil, err + } + + return gen.GetDashboardSummary200JSONResponse(resp), nil +} diff --git a/internal/httpapi/gen/api.gen.go b/internal/httpapi/gen/api.gen.go index fb3bf38..bd9ae68 100644 --- a/internal/httpapi/gen/api.gen.go +++ b/internal/httpapi/gen/api.gen.go @@ -153,6 +153,14 @@ const ( ExecutionStatusVerifying ExecutionStatus = "verifying" ) +// Defines values for GraphViewHealth. +const ( + GraphViewHealthDegraded GraphViewHealth = "degraded" + GraphViewHealthDown GraphViewHealth = "down" + GraphViewHealthHealthy GraphViewHealth = "healthy" + GraphViewHealthUnknown GraphViewHealth = "unknown" +) + // Defines values for HealthSummaryEntitiesHealth. const ( HealthSummaryEntitiesHealthDegraded HealthSummaryEntitiesHealth = "degraded" @@ -252,10 +260,10 @@ const ( // Defines values for TrendDirection. const ( - Degrading TrendDirection = "degrading" - Improving TrendDirection = "improving" - Stable TrendDirection = "stable" - Unknown TrendDirection = "unknown" + TrendDirectionDegrading TrendDirection = "degrading" + TrendDirectionImproving TrendDirection = "improving" + TrendDirectionStable TrendDirection = "stable" + TrendDirectionUnknown TrendDirection = "unknown" ) // Defines values for ListApprovalsParamsStatus. @@ -295,6 +303,11 @@ const ( Out GetEntityRelationsParamsDirection = "out" ) +// Defines values for GetGraphParamsInclude. +const ( + Status GetGraphParamsInclude = "status" +) + // Defines values for QueryMetricsParamsRollup. const ( QueryMetricsParamsRollupAuto QueryMetricsParamsRollup = "auto" @@ -520,6 +533,35 @@ type ClientSecrets struct { Keys []string `json:"keys"` } +// DashboardSummary defines model for DashboardSummary. +type DashboardSummary struct { + ApprovalsPending int `json:"approvals_pending"` + + // EntitiesByState entity counts keyed by state + EntitiesByState map[string]int `json:"entities_by_state"` + + // EntitiesByType entity counts keyed by type + EntitiesByType map[string]int `json:"entities_by_type"` + + // EventRate event counts bucketed by 5-minute interval, most recent last + EventRate []struct { + Bucket time.Time `json:"bucket"` + Count int `json:"count"` + } `json:"event_rate"` + + // ExecutionsByState execution counts keyed by state, last 24h + ExecutionsByState map[string]int `json:"executions_by_state"` + Health struct { + Degraded int `json:"degraded"` + Down int `json:"down"` + Healthy int `json:"healthy"` + Unknown int `json:"unknown"` + } `json:"health"` + + // SignalsBySeverity open (non-resolved) signal counts keyed by severity + SignalsBySeverity map[string]int `json:"signals_by_severity"` +} + // EnrollRequest defines model for EnrollRequest. type EnrollRequest struct { // Hostname Actual hostname of the enrolling machine @@ -696,12 +738,18 @@ type ExecutionRequest struct { // GraphView defines model for GraphView. type GraphView struct { Edges []Relationship `json:"edges"` - Nodes []Entity `json:"nodes"` + + // Health entity id -> health, present when include=status was requested + Health *map[string]GraphViewHealth `json:"health,omitempty"` + Nodes []Entity `json:"nodes"` // Truncated True if node cap was hit Truncated *bool `json:"truncated,omitempty"` } +// GraphViewHealth defines model for GraphView.Health. +type GraphViewHealth string + // HealthSummary defines model for HealthSummary. type HealthSummary struct { Entities []struct { @@ -1234,8 +1282,14 @@ type GetGraphParams struct { Root *string `form:"root,omitempty" json:"root,omitempty"` Depth *int `form:"depth,omitempty" json:"depth,omitempty"` RelType *[]string `form:"rel_type,omitempty" json:"rel_type,omitempty"` + + // Include include=status joins entity_status and populates GraphView.health + Include *[]GetGraphParamsInclude `form:"include,omitempty" json:"include,omitempty"` } +// GetGraphParamsInclude defines parameters for GetGraph. +type GetGraphParamsInclude string + // SearchKnowledgeParams defines parameters for SearchKnowledge. type SearchKnowledgeParams struct { Q string `form:"q" json:"q"` @@ -1469,6 +1523,9 @@ type ServerInterface interface { // List secrets accessible to this client // (GET /clients/{slug}/secrets) GetClientSecrets(w http.ResponseWriter, r *http.Request, slug EntitySlug) + // One-round-trip overview for the control room home page + // (GET /dashboard/summary) + GetDashboardSummary(w http.ResponseWriter, r *http.Request) // List entities // (GET /entities) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) @@ -1664,6 +1721,12 @@ func (_ Unimplemented) GetClientSecrets(w http.ResponseWriter, r *http.Request, w.WriteHeader(http.StatusNotImplemented) } +// One-round-trip overview for the control room home page +// (GET /dashboard/summary) +func (_ Unimplemented) GetDashboardSummary(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // List entities // (GET /entities) func (_ Unimplemented) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) { @@ -2537,6 +2600,26 @@ func (siw *ServerInterfaceWrapper) GetClientSecrets(w http.ResponseWriter, r *ht handler.ServeHTTP(w, r) } +// GetDashboardSummary operation middleware +func (siw *ServerInterfaceWrapper) GetDashboardSummary(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + ctx = context.WithValue(ctx, BearerAuthScopes, []string{}) + + r = r.WithContext(ctx) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetDashboardSummary(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // ListEntities operation middleware func (siw *ServerInterfaceWrapper) ListEntities(w http.ResponseWriter, r *http.Request) { @@ -3305,6 +3388,14 @@ func (siw *ServerInterfaceWrapper) GetGraph(w http.ResponseWriter, r *http.Reque return } + // ------------- Optional query parameter "include" ------------- + + err = runtime.BindQueryParameter("form", true, false, "include", r.URL.Query(), ¶ms.Include) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "include", Err: err}) + return + } + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { siw.Handler.GetGraph(w, r, params) })) @@ -4547,6 +4638,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/clients/{slug}/secrets", wrapper.GetClientSecrets) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/dashboard/summary", wrapper.GetDashboardSummary) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/entities", wrapper.ListEntities) }) @@ -5020,6 +5114,34 @@ func (response GetClientSecretsdefaultApplicationProblemPlusJSONResponse) VisitG return json.NewEncoder(w).Encode(response.Body) } +type GetDashboardSummaryRequestObject struct { +} + +type GetDashboardSummaryResponseObject interface { + VisitGetDashboardSummaryResponse(w http.ResponseWriter) error +} + +type GetDashboardSummary200JSONResponse DashboardSummary + +func (response GetDashboardSummary200JSONResponse) VisitGetDashboardSummaryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + + return json.NewEncoder(w).Encode(response) +} + +type GetDashboardSummarydefaultApplicationProblemPlusJSONResponse struct { + Body Problem + StatusCode int +} + +func (response GetDashboardSummarydefaultApplicationProblemPlusJSONResponse) VisitGetDashboardSummaryResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/problem+json") + w.WriteHeader(response.StatusCode) + + return json.NewEncoder(w).Encode(response.Body) +} + type ListEntitiesRequestObject struct { Params ListEntitiesParams } @@ -6401,6 +6523,9 @@ type StrictServerInterface interface { // List secrets accessible to this client // (GET /clients/{slug}/secrets) GetClientSecrets(ctx context.Context, request GetClientSecretsRequestObject) (GetClientSecretsResponseObject, error) + // One-round-trip overview for the control room home page + // (GET /dashboard/summary) + GetDashboardSummary(ctx context.Context, request GetDashboardSummaryRequestObject) (GetDashboardSummaryResponseObject, error) // List entities // (GET /entities) ListEntities(ctx context.Context, request ListEntitiesRequestObject) (ListEntitiesResponseObject, error) @@ -6870,6 +6995,30 @@ func (sh *strictHandler) GetClientSecrets(w http.ResponseWriter, r *http.Request } } +// GetDashboardSummary operation middleware +func (sh *strictHandler) GetDashboardSummary(w http.ResponseWriter, r *http.Request) { + var request GetDashboardSummaryRequestObject + + handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) { + return sh.ssi.GetDashboardSummary(ctx, request.(GetDashboardSummaryRequestObject)) + } + for _, middleware := range sh.middlewares { + handler = middleware(handler, "GetDashboardSummary") + } + + response, err := handler(r.Context(), w, r, request) + + if err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } else if validResponse, ok := response.(GetDashboardSummaryResponseObject); ok { + if err := validResponse.VisitGetDashboardSummaryResponse(w); err != nil { + sh.options.ResponseErrorHandlerFunc(w, r, err) + } + } else if response != nil { + sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response)) + } +} + // ListEntities operation middleware func (sh *strictHandler) ListEntities(w http.ResponseWriter, r *http.Request, params ListEntitiesParams) { var request ListEntitiesRequestObject @@ -8031,166 +8180,173 @@ func (sh *strictHandler) GetTrends(w http.ResponseWriter, r *http.Request, entit // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/internal/httpapi/impl.go b/internal/httpapi/impl.go index e0ab0bb..3903819 100644 --- a/internal/httpapi/impl.go +++ b/internal/httpapi/impl.go @@ -322,9 +322,44 @@ func (s *Server) GetGraph(ctx context.Context, req gen.GetGraphRequestObject) (g if truncated { resp.Truncated = &truncated } + + if req.Params.Include != nil { + for _, inc := range *req.Params.Include { + if inc == gen.Status { + health, herr := s.entityHealthByID(ctx, ids) + if herr != nil { + return nil, herr + } + resp.Health = &health + break + } + } + } + return resp, nil } +// entityHealthByID returns entity_status.health keyed by entity id, for the +// given id set (used by GetGraph's include=status). +func (s *Server) entityHealthByID(ctx context.Context, ids []uuid.UUID) (map[string]gen.GraphViewHealth, error) { + health := make(map[string]gen.GraphViewHealth, len(ids)) + rows, err := s.pool.Query(ctx, + `SELECT entity_id, health FROM entity_status WHERE entity_id = ANY($1)`, ids) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + var id uuid.UUID + var h string + if err := rows.Scan(&id, &h); err != nil { + return nil, err + } + health[id.String()] = gen.GraphViewHealth(h) + } + return health, rows.Err() +} + func (s *Server) queryEntities(ctx context.Context, query string, args ...any) ([]gen.Entity, error) { rows, err := s.pool.Query(ctx, query, args...) if err != nil { diff --git a/internal/httpapi/phase3.go b/internal/httpapi/phase3.go index 6bdb406..90c792f 100644 --- a/internal/httpapi/phase3.go +++ b/internal/httpapi/phase3.go @@ -1835,10 +1835,6 @@ func (s *Server) QueryMetrics(ctx context.Context, req gen.QueryMetricsRequestOb if req.Params.EntityId == nil || *req.Params.EntityId == "" { return nil, fmt.Errorf("%w: entity_id is required", domain.ErrInvalidInput) } - if req.Params.Metric == nil || len(*req.Params.Metric) == 0 { - return nil, fmt.Errorf("%w: metric is required", domain.ErrInvalidInput) - } - entityID, err := s.resolveEntityID(ctx, *req.Params.EntityId) if err != nil { return nil, err @@ -1853,8 +1849,33 @@ func (s *Server) QueryMetrics(ctx context.Context, req gen.QueryMetricsRequestOb to = *req.Params.To } + var metricNames []string + if req.Params.Metric != nil && len(*req.Params.Metric) > 0 { + metricNames = *req.Params.Metric + } else { + // metric omitted: report every metric recorded for this entity in range. + rows, err := s.pool.Query(ctx, ` + SELECT DISTINCT metric FROM metric_samples + WHERE entity_id = $1 AND ts >= $2 AND ts <= $3 + ORDER BY metric`, entityID, from, to) + if err != nil { + return nil, err + } + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + rows.Close() + return nil, err + } + metricNames = append(metricNames, name) + } + if err := rows.Err(); err != nil { + return nil, err + } + } + items := []gen.MetricSeries{} - for _, metricName := range *req.Params.Metric { + for _, metricName := range metricNames { series := gen.MetricSeries{ EntityId: entityID.String(), Metric: metricName, @@ -1953,14 +1974,14 @@ func (s *Server) GetTrends(ctx context.Context, req gen.GetTrendsRequestObject) f, _ := slopeNum.Float64Value() t.Slope = float32Ptr(float32(f.Float64)) if f.Float64 > 0.01 { - t.Direction = gen.Improving + t.Direction = gen.TrendDirectionImproving } else if f.Float64 < -0.01 { - t.Direction = gen.Degrading + t.Direction = gen.TrendDirectionDegrading } else { - t.Direction = gen.Stable + t.Direction = gen.TrendDirectionStable } } else { - t.Direction = gen.Unknown + t.Direction = gen.TrendDirectionUnknown } items = append(items, t) } diff --git a/plans/2026-07-08-control-room-webui.md b/plans/2026-07-08-control-room-webui.md index b0fe630..f7ca436 100644 --- a/plans/2026-07-08-control-room-webui.md +++ b/plans/2026-07-08-control-room-webui.md @@ -1,6 +1,20 @@ # 2026-07-08 — Control room web UI -**Status:** Planned +**Status:** In Progress — N0-N3 (Nomos amendment: chat home + sessions), M1 +(dashboard/summary, Overview, Entities table, live event feed, shadcn-svelte +component system), M2 (Operations ledger with approve/deny + cancel, Signals +page with ack/resolve/mute, live nav badges), and M3 (graph explorer with +`include=status` health coloring, per-relationship-type edge coloring + +legend/filter, node-type filter, node search/highlight, entity detail page +with uPlot metric charts, `#/entity/:slug` route) complete 2026-07-08. Event +gap-fill (M2's +other half) landed earlier in commit e8e230b, and approval creation's FK bug +(gaps-plan A1) was already fixed, unblocking M2. While building M3, also +fixed `GET /metrics` to make the `metric` query param genuinely optional +(server now reports every metric recorded for the entity in range) — the +implementation previously 400'd when it was omitted, contradicting its own +documented-optional spec. M4 (agent activity, knowledge search page, audit, +correlation grouping, polish) remains. ## Goal @@ -31,8 +45,18 @@ Dependencies kept minimal: - `d3-force` — graph physics only; render SVG/canvas by hand - `uPlot` — ~45 KB canvas time-series, ideal for `/metrics` rollups - `openapi-typescript` — dev-only, generates `api-types.d.ts` -- No SvelteKit (no SSR wanted — the Go binary is the server), no component - framework; hash router; hand-rolled dark-theme CSS. +- No SvelteKit (no SSR wanted — the Go binary is the server); hash router. + +*Amendment 2026-07-08 (M1):* component library is +[shadcn-svelte](https://www.shadcn-svelte.com/) over Tailwind CSS v4 +(`@tailwindcss/vite`), not hand-rolled CSS — Table, Card, Badge, Sidebar, +Sheet, Select, Input, Button, Tabs, ScrollArea, Tooltip, Dialog, +Dropdown-menu, Sonner installed via `npx shadcn-svelte add`. The existing +dark GitHub-style palette (`app.css`) was ported into shadcn's CSS-variable +theme contract (`--background`, `--card`, `--primary`, etc. under +`@theme inline`) so old and new components share one palette. `d3-force` / +`uPlot` remain the plan for the graph/charts milestones (M3), unaffected by +this change. Build integration: commit a placeholder `web/dist/index.html` so backend-only `go build` never breaks; `make ui` runs the Vite build; add a node stage to diff --git a/plans/index.md b/plans/index.md index f0076bf..9c607b1 100644 --- a/plans/index.md +++ b/plans/index.md @@ -11,7 +11,7 @@ went sideways, open an investigation. | 2026-07-05 | [Oikos Prometheus LXC](2026-07-05-oikos-prometheus-lxc.md) | Planned | | 2026-07-08 | [Plan vs implementation cross-reference](2026-07-08-plan-implementation-audit.md) | Planned | | 2026-07-08 | [Oikos gaps, broken things, and improvements](2026-07-08-oikos-gaps-and-improvements.md) | Planned | -| 2026-07-08 | [Control room web UI](2026-07-08-control-room-webui.md) | Planned | +| 2026-07-08 | [Control room web UI](2026-07-08-control-room-webui.md) | In Progress | | 2026-07-08 | [Nomos resident agent (renames Hermes)](2026-07-08-nomos-resident-agent.md) | In Progress | ## Done diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..0094c85 --- /dev/null +++ b/web/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "style": "vega", + "tailwind": { + "css": "src/app.css", + "baseColor": "zinc" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/web/package-lock.json b/web/package-lock.json index 4708d06..490bb7f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7,10 +7,27 @@ "": { "name": "oikos-web", "version": "0.1.0", + "dependencies": { + "clsx": "^2.1.1", + "d3-force": "^3.0.0", + "dompurify": "^3.4.11", + "marked": "^18.0.5", + "tailwind-merge": "^3.6.0", + "uplot": "^1.6.32" + }, "devDependencies": { + "@internationalized/date": "^3.12.2", + "@lucide/svelte": "^1.23.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.3.2", "@tsconfig/svelte": "^5.0.0", + "@types/d3-force": "^3.0.10", + "bits-ui": "^2.18.1", + "mode-watcher": "^1.1.0", "svelte": "^5.0.0", + "svelte-sonner": "^1.1.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.3.2", "typescript": "^5.5.0", "vite": "^6.0.0" } @@ -457,6 +474,44 @@ "node": ">=18" } }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@internationalized/date": { + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.2.tgz", + "integrity": "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -507,6 +562,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lucide/svelte": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-1.23.0.tgz", + "integrity": "sha512-3LQbKXx9vId6Nx4E2Nu2qwgJfdmr5+CVeVJbxe5cy+HcnCRd9QVVtZXqvgBYAV1OJrPmQAf9/3gJWLCpASC/Ng==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.2", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", @@ -907,6 +972,288 @@ "vite": "^6.0.0" } }, + "node_modules/@swc/helpers": { + "version": "0.5.23", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", + "integrity": "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.2.tgz", + "integrity": "sha512-yWP/sqEcBLaD8JuA6zNwxoYKr75qxTioYwlRwekj5Jr/I5GXnoJfjetH/psLUIv74cYTH2lBUEzBkinthoYcBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.2.tgz", + "integrity": "sha512-z8ZgnzX8gdNoWLBLqBPoh/sjnxkwvf9ZuWjnO0l0yIzbLa5/9S+eC5QxGZKRobVHIC3/1BoMWjHblqWjcgFgag==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.2.tgz", + "integrity": "sha512-QI9BO7KlNZsp2GuO0jwAAj5jCDABOKXRkCk2XuKTSaNEFSdfzqswYVTtCHBNKHLsqyjFyFkqlDiwkNbTYSssMQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.2.tgz", + "integrity": "sha512-eHpMeX4JXfVNJDEcsouTeCBubJBTcTLigeaw/NTUW6PB5ATKKXdyonnXgTBX2VuRbjz1hjfz6C5XAhr52ImQXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -914,6 +1261,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -925,7 +1279,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/acorn": { @@ -961,16 +1315,81 @@ "node": ">= 0.4" } }, + "node_modules/bits-ui": { + "version": "2.18.1", + "resolved": "https://registry.npmjs.org/bits-ui/-/bits-ui-2.18.1.tgz", + "integrity": "sha512-KkemzKFH4T3gt3H+P86JcnAWExjByv/6vlwjm/BoCwTPHu03yiCdxbghdJLvFReQTe0acCAiRcKfmixxD6XvlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/dom": "^1.7.1", + "esm-env": "^1.1.2", + "runed": "^0.35.1", + "svelte-toolbelt": "^0.10.6", + "tabbable": "^6.2.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/huntabyte" + }, + "peerDependencies": { + "@internationalized/date": "^3.8.1", + "svelte": "^5.33.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -999,6 +1418,26 @@ "node": ">=0.10.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -1006,6 +1445,29 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.21.6", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.6.tgz", + "integrity": "sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -1106,6 +1568,20 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "dev": true, + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -1116,6 +1592,16 @@ "@types/estree": "^1.0.6" } }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/kleur": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", @@ -1126,6 +1612,267 @@ "node": ">=6" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -1133,6 +1880,16 @@ "dev": true, "license": "MIT" }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1143,6 +1900,85 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mode-watcher": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.1.0.tgz", + "integrity": "sha512-mUT9RRGPDYenk59qJauN1rhsIMKBmWA3xMF+uRwE8MW/tjhaDSCCARqkSuDTq8vr4/2KcAxIGVjACxTjdk5C3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, + "node_modules/mode-watcher/node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/mode-watcher/node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1263,6 +2099,31 @@ "fsevents": "~2.3.2" } }, + "node_modules/runed": { + "version": "0.35.1", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.35.1.tgz", + "integrity": "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "esm-env": "^1.0.0", + "lz-string": "^1.5.0" + }, + "peerDependencies": { + "@sveltejs/kit": "^2.21.0", + "svelte": "^5.7.0" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1273,6 +2134,16 @@ "node": ">=0.10.0" } }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/svelte": { "version": "5.56.4", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.4.tgz", @@ -1301,6 +2172,115 @@ "node": ">=18" } }, + "node_modules/svelte-sonner": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/svelte-sonner/-/svelte-sonner-1.1.1.tgz", + "integrity": "sha512-5cd3p7wa4cq0NsqslMwdlPb7x1JglEZ/GKrLePWNr5bCxR1nagAVrY01FRFrXfUGs41miLt3C327+8XJo5BzZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "runed": "^0.28.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-sonner/node_modules/runed": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.28.0.tgz", + "integrity": "sha512-k2xx7RuO9hWcdd9f+8JoBeqWtYrm5CALfgpkg2YDB80ds/QE4w0qqu34A7fqiAwiBBSBQOid7TLxwxVC27ymWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "license": "MIT", + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, + "node_modules/svelte-toolbelt": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.10.6.tgz", + "integrity": "sha512-YWuX+RE+CnWYx09yseAe4ZVMM7e7GRFZM6OYWpBKOb++s+SQ8RBIMMe+Bs/CznBMc0QPLjr+vDBxTAkozXsFXQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.35.1", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.30.2" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz", + "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwind-merge": ">=3.0.0", + "tailwindcss": "*" + }, + "peerDependenciesMeta": { + "tailwind-merge": { + "optional": true + } + } + }, + "node_modules/tailwindcss": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.2.tgz", + "integrity": "sha512-WtctNNSH8A9jlMIqxzuYumOHU5uGZyRv0Q5svQl+oEPy5w84YpBxdb7MdqyiSPQge5jTJ6zFQLq0PFygdccSBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -1318,6 +2298,13 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1332,6 +2319,12 @@ "node": ">=14.17" } }, + "node_modules/uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", diff --git a/web/package.json b/web/package.json index de54a56..95836fd 100644 --- a/web/package.json +++ b/web/package.json @@ -9,10 +9,27 @@ "preview": "vite preview" }, "devDependencies": { + "@internationalized/date": "^3.12.2", + "@lucide/svelte": "^1.23.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@tailwindcss/vite": "^4.3.2", "@tsconfig/svelte": "^5.0.0", + "@types/d3-force": "^3.0.10", + "bits-ui": "^2.18.1", + "mode-watcher": "^1.1.0", "svelte": "^5.0.0", + "svelte-sonner": "^1.1.1", + "tailwind-variants": "^3.2.2", + "tailwindcss": "^4.3.2", "typescript": "^5.5.0", "vite": "^6.0.0" + }, + "dependencies": { + "clsx": "^2.1.1", + "d3-force": "^3.0.0", + "dompurify": "^3.4.11", + "marked": "^18.0.5", + "tailwind-merge": "^3.6.0", + "uplot": "^1.6.32" } } diff --git a/web/src/App.svelte b/web/src/App.svelte index b3a123c..c1e3bbe 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -1,155 +1,212 @@ -
- + + + + + navigate('chat')} tooltipContent="Chat"> + {#snippet child({ props })} + + {/snippet} + + + {#each navItems as item} + + navigate(item.id)} + tooltipContent={item.label} + > + {#snippet child({ props })} + + {/snippet} + + {#if item.badge?.()} + {item.badge()} + {/if} + + {/each} + + + -
- {#if page === 'chat'} - - {:else if page === 'sessions'} - - {:else} - - {/if} -
+ + + + - {#if drawerOpen} - - {/if} -
+ +
+ + {page === 'entity' ? routeParam : page} +
+ {#if $summary} + + {#if approvalsPending} + + {/if} + {#if openSignals} + + {/if} + {/if} + +
+
+ {#if page === 'overview'} + + {:else if page === 'entities'} + + {:else if page === 'graph'} + + {:else if page === 'entity' && routeParam} + + {:else if page === 'ops'} + + {:else if page === 'signals'} + + {:else if page === 'events'} + + {:else if page === 'sessions'} + + {:else if page === 'agent'} + + {:else if page === 'knowledge'} + + {:else if page === 'audit'} + + {:else} + + {/if} +
+
+ - + + + + Nomos chat + Persistent chat drawer + +
+ +
+
+
diff --git a/web/src/app.css b/web/src/app.css index 08008af..56f9152 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,33 +1,110 @@ +@import 'tailwindcss'; + +@custom-variant dark (&:is(.dark *)); + :root { - --bg: #0d1117; - --bg-surface: #161b22; - --bg-deeper: #0a0e13; - --bg-hover: #21262d; - --bg-active: #292e36; + --radius: 0.5rem; + --background: #0d1117; + --foreground: #e6edf3; + --card: #161b22; + --card-foreground: #e6edf3; + --popover: #161b22; + --popover-foreground: #e6edf3; + --primary: #58a6ff; + --primary-foreground: #0d1117; + --secondary: #21262d; + --secondary-foreground: #e6edf3; + --muted: #21262d; + --muted-foreground: #8b949e; + --accent: #292e36; + --accent-foreground: #e6edf3; + --destructive: #f85149; + --destructive-foreground: #ffffff; + --success: #3fb950; + --warning: #d29922; --border: #30363d; - --text: #e6edf3; - --text-muted: #8b949e; - --accent-blue: #58a6ff; - --accent-green: #3fb950; - --accent-red: #f85149; - --accent-orange: #d29922; + --input: #30363d; + --ring: #58a6ff; + --sidebar: #161b22; + --sidebar-foreground: #e6edf3; + --sidebar-primary: #58a6ff; + --sidebar-primary-foreground: #0d1117; + --sidebar-accent: #21262d; + --sidebar-accent-foreground: #e6edf3; + --sidebar-border: #30363d; + --sidebar-ring: #58a6ff; --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; + + /* legacy aliases still referenced by Chat/Sessions/App */ + --bg: var(--background); + --bg-surface: var(--card); + --bg-deeper: #0a0e13; + --bg-hover: var(--secondary); + --bg-active: var(--accent); + --text: var(--foreground); + --text-muted: var(--muted-foreground); + --accent-blue: var(--primary); + --accent-green: var(--success); + --accent-red: var(--destructive); + --accent-orange: var(--warning); } -*, *::before, *::after { - box-sizing: border-box; - margin: 0; - padding: 0; +/* the app is dark-only; treat root as the dark theme unconditionally */ +.dark { + --background: #0d1117; + --foreground: #e6edf3; } -html, body { - height: 100%; - background: var(--bg); - color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - font-size: 14px; - line-height: 1.4; - -webkit-font-smoothing: antialiased; +@theme inline { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-success: var(--success); + --color-warning: var(--warning); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --font-mono: var(--font-mono); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + html, + body { + @apply bg-background text-foreground; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + font-size: 14px; + line-height: 1.4; + -webkit-font-smoothing: antialiased; + } } #app { diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index 8ae0261..a2b0b86 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -1,4 +1,5 @@ const BASE = '/agent' +const API = '/api/v1' export interface Session { id: string @@ -90,3 +91,384 @@ export function streamChat( return controller } + +export interface DashboardSummary { + entities_by_type: Record + entities_by_state: Record + health: { healthy: number; degraded: number; down: number; unknown: number } + signals_by_severity: Record + approvals_pending: number + executions_by_state: Record + event_rate: { bucket: string; count: number }[] +} + +export async function fetchDashboardSummary(): Promise { + const res = await fetch(`${API}/dashboard/summary`) + if (!res.ok) return null + return res.json() +} + +export interface Entity { + id: string + slug: string + type: string + name: string + state?: string | null + attributes: Record + version: number + created_at: string + updated_at: string +} + +export interface EntityFilters { + type?: string + state?: string + q?: string +} + +export async function fetchEntities(filters: EntityFilters = {}): Promise { + const params = new URLSearchParams() + if (filters.type) params.set('type', filters.type) + if (filters.state) params.set('state', filters.state) + if (filters.q) params.set('q', filters.q) + params.set('limit', '200') + const res = await fetch(`${API}/entities?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export interface EventFilters { + type?: string + severity?: string +} + +export async function fetchEvents(filters: EventFilters = {}): Promise { + const params = new URLSearchParams() + if (filters.type) params.set('type', filters.type) + if (filters.severity) params.set('severity', filters.severity) + params.set('limit', '100') + const res = await fetch(`${API}/events?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export interface Approval { + id: string + slug: string + subject?: string | null + action: string + risk_class: string + kind: 'execution' | 'policy-change' | 'pattern-activation' + payload?: Record | null + status: 'pending' | 'approved' | 'denied' | 'expired' | 'revoked' + expires_at: string + decided_at?: string | null + decided_by?: string | null + created_at: string +} + +export async function fetchApprovals(status?: string): Promise { + const params = new URLSearchParams() + if (status) params.set('status', status) + params.set('limit', '200') + const res = await fetch(`${API}/approvals?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function decideApproval( + id: string, + decision: 'approve' | 'deny' | 'revoke', + note?: string +): Promise { + const res = await fetch(`${API}/approvals/${id}/decision`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ decision, note }) + }) + if (!res.ok) return null + return res.json() +} + +export interface Execution { + id: string + slug: string + target?: string | null + action: string + risk_class: string + status: string + approval_id?: string | null + agent_id?: string | null + result?: Record | null + duration_ms?: number | null + verified: boolean + correlation_id: string + started_at?: string | null + completed_at?: string | null + created_at: string +} + +export async function fetchExecutions(status?: string): Promise { + const params = new URLSearchParams() + if (status) params.set('status', status) + params.set('limit', '200') + const res = await fetch(`${API}/executions?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function cancelExecution(id: string): Promise { + const res = await fetch(`${API}/executions/${id}/cancel`, { method: 'POST' }) + if (!res.ok) return null + return res.json() +} + +export interface Signal { + id: string + slug: string + kind: string + severity: 'info' | 'warning' | 'critical' + state: 'raised' | 'acknowledged' | 'acting' | 'muted' | 'resolved' | 'failed' + target?: string | null + evidence?: string | null + likely_cause?: string | null + occurrence_count: number + flap_count: number + hold_down_until?: string | null + mute_until?: string | null + first_seen_at: string + last_seen_at: string +} + +export async function fetchSignals(filters: { state?: string; severity?: string } = {}): Promise { + const params = new URLSearchParams() + if (filters.state) params.set('state', filters.state) + if (filters.severity) params.set('severity', filters.severity) + params.set('limit', '200') + const res = await fetch(`${API}/signals?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function ackSignal(id: string): Promise { + const res = await fetch(`${API}/signals/${id}/ack`, { method: 'POST' }) + if (!res.ok) return null + return res.json() +} + +export async function resolveSignal(id: string, note?: string): Promise { + const res = await fetch(`${API}/signals/${id}/resolve`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ note }) + }) + if (!res.ok) return null + return res.json() +} + +export async function muteSignal(id: string, muteUntil: string, note?: string): Promise { + const res = await fetch(`${API}/signals/${id}/mute`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ mute_until: muteUntil, note }) + }) + if (!res.ok) return null + return res.json() +} + +export interface Relationship { + source: string + target: string + type: string + attributes?: Record | null + valid_from: string + valid_to?: string | null +} + +export type Health = 'healthy' | 'degraded' | 'down' | 'unknown' + +export interface GraphView { + nodes: Entity[] + edges: Relationship[] + truncated?: boolean + health?: Record +} + +export interface GraphFilters { + root?: string + depth?: number + relType?: string[] + includeStatus?: boolean +} + +export async function fetchGraph(filters: GraphFilters = {}): Promise { + const params = new URLSearchParams() + if (filters.root) params.set('root', filters.root) + if (filters.depth) params.set('depth', String(filters.depth)) + for (const rt of filters.relType ?? []) params.append('rel_type', rt) + if (filters.includeStatus) params.append('include', 'status') + const res = await fetch(`${API}/graph?${params}`) + if (!res.ok) return null + return res.json() +} + +export interface BlastRadiusItem { + entity: Entity + depth: number +} + +export async function fetchBlastRadius(id: string): Promise { + const res = await fetch(`${API}/entities/${id}/blast-radius`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function fetchEntity(id: string): Promise { + const res = await fetch(`${API}/entities/${id}`) + if (!res.ok) return null + return res.json() +} + +export interface MetricSample { + ts: string + value?: number | null + avg?: number | null + min?: number | null + max?: number | null +} + +export interface MetricSeries { + entity_id: string + metric: string + rollup: 'raw' | '1h' | '1d' + samples: MetricSample[] +} + +export async function fetchMetrics(entityId: string): Promise { + const params = new URLSearchParams({ entity_id: entityId, rollup: 'auto' }) + const res = await fetch(`${API}/metrics?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export interface KnowledgeHit { + id: string + slug: string + type: 'document' | 'runbook' | 'investigation' + title: string +} + +export async function fetchEntityKnowledge(entityId: string): Promise { + const res = await fetch(`${API}/knowledge/${entityId}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function fetchEntityEvents(entityId: string): Promise { + const params = new URLSearchParams({ entity_id: entityId, limit: '50' }) + const res = await fetch(`${API}/events?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function fetchEntitySignals(entityId: string): Promise { + const params = new URLSearchParams({ entity_id: entityId, limit: '50' }) + const res = await fetch(`${API}/signals?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function fetchEntityExecutions(entityId: string): Promise { + const params = new URLSearchParams({ target: entityId, limit: '50' }) + const res = await fetch(`${API}/executions?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export interface AgentActivity { + id: number + ts: string + agent_id: string + session_id?: string | null + activity_type: 'tool_call' | 'reasoning' | 'decision' | 'mcp_query' | 'escalation' + tool_name?: string | null + entity_id?: string | null + input_summary?: string | null + output_summary?: string | null + duration_ms?: number | null + token_count?: number | null + success?: boolean | null + correlation_id?: string | null +} + +export async function fetchAgentActivity(filters: { + agent_id?: string + activity_type?: string + entity_id?: string + limit?: number +} = {}): Promise { + const params = new URLSearchParams() + if (filters.agent_id) params.set('agent_id', filters.agent_id) + if (filters.activity_type) params.set('activity_type', filters.activity_type) + if (filters.entity_id) params.set('entity_id', filters.entity_id) + params.set('limit', String(filters.limit ?? 200)) + const res = await fetch(`${API}/agent-activity?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export async function searchKnowledge(q: string, limit = 50): Promise { + const params = new URLSearchParams({ q, limit: String(limit) }) + const res = await fetch(`${API}/knowledge/search?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} + +export interface AuditEntry { + id: number + ts: string + actor_type: 'agent' | 'operator' | 'system' | 'scheduler' + actor_id?: string | null + action: string + entity_id?: string | null + method?: string | null + path?: string | null + status_code?: number | null + detail?: Record + source_ip?: string | null + correlation_id?: string | null +} + +export async function fetchAudit(filters: { + actor_type?: string + actor_id?: string + entity_id?: string + action?: string + correlation_id?: string + limit?: number +} = {}): Promise { + const params = new URLSearchParams() + if (filters.actor_type) params.set('actor_type', filters.actor_type) + if (filters.actor_id) params.set('actor_id', filters.actor_id) + if (filters.entity_id) params.set('entity_id', filters.entity_id) + if (filters.action) params.set('action', filters.action) + if (filters.correlation_id) params.set('correlation_id', filters.correlation_id) + params.set('limit', String(filters.limit ?? 200)) + const res = await fetch(`${API}/audit?${params}`) + if (!res.ok) return [] + const data = await res.json() + return data.items ?? [] +} diff --git a/web/src/lib/components/ContextRail.svelte b/web/src/lib/components/ContextRail.svelte new file mode 100644 index 0000000..cc06bcf --- /dev/null +++ b/web/src/lib/components/ContextRail.svelte @@ -0,0 +1,109 @@ + + + diff --git a/web/src/lib/components/ui/badge/badge.svelte b/web/src/lib/components/ui/badge/badge.svelte new file mode 100644 index 0000000..51bbc23 --- /dev/null +++ b/web/src/lib/components/ui/badge/badge.svelte @@ -0,0 +1,49 @@ + + + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/badge/index.ts b/web/src/lib/components/ui/badge/index.ts new file mode 100644 index 0000000..64e0aa9 --- /dev/null +++ b/web/src/lib/components/ui/badge/index.ts @@ -0,0 +1,2 @@ +export { default as Badge } from "./badge.svelte"; +export { badgeVariants, type BadgeVariant } from "./badge.svelte"; diff --git a/web/src/lib/components/ui/button/button.svelte b/web/src/lib/components/ui/button/button.svelte new file mode 100644 index 0000000..c38862e --- /dev/null +++ b/web/src/lib/components/ui/button/button.svelte @@ -0,0 +1,82 @@ + + + + +{#if href} + + {@render children?.()} + +{:else} + +{/if} diff --git a/web/src/lib/components/ui/button/index.ts b/web/src/lib/components/ui/button/index.ts new file mode 100644 index 0000000..fb585d7 --- /dev/null +++ b/web/src/lib/components/ui/button/index.ts @@ -0,0 +1,17 @@ +import Root, { + type ButtonProps, + type ButtonSize, + type ButtonVariant, + buttonVariants, +} from "./button.svelte"; + +export { + Root, + type ButtonProps as Props, + // + Root as Button, + buttonVariants, + type ButtonProps, + type ButtonSize, + type ButtonVariant, +}; diff --git a/web/src/lib/components/ui/card/card-action.svelte b/web/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..7c48844 --- /dev/null +++ b/web/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/card-content.svelte b/web/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..082a786 --- /dev/null +++ b/web/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/card-description.svelte b/web/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/web/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/web/src/lib/components/ui/card/card-footer.svelte b/web/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..591c3f7 --- /dev/null +++ b/web/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/card-header.svelte b/web/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..21e9a17 --- /dev/null +++ b/web/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/card-title.svelte b/web/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7d20243 --- /dev/null +++ b/web/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/card.svelte b/web/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..329a6c7 --- /dev/null +++ b/web/src/lib/components/ui/card/card.svelte @@ -0,0 +1,22 @@ + + +
img:first-child]:pt-0 data-[size=sm]:gap-4 data-[size=sm]:py-4 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl group/card flex flex-col", className)} + {...restProps} +> + {@render children?.()} +
diff --git a/web/src/lib/components/ui/card/index.ts b/web/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..4d3fce4 --- /dev/null +++ b/web/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction, +}; diff --git a/web/src/lib/components/ui/collapsible/collapsible-content.svelte b/web/src/lib/components/ui/collapsible/collapsible-content.svelte new file mode 100644 index 0000000..bdabb55 --- /dev/null +++ b/web/src/lib/components/ui/collapsible/collapsible-content.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte b/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte new file mode 100644 index 0000000..ece7ad6 --- /dev/null +++ b/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/collapsible/collapsible.svelte b/web/src/lib/components/ui/collapsible/collapsible.svelte new file mode 100644 index 0000000..39cdd4e --- /dev/null +++ b/web/src/lib/components/ui/collapsible/collapsible.svelte @@ -0,0 +1,11 @@ + + + diff --git a/web/src/lib/components/ui/collapsible/index.ts b/web/src/lib/components/ui/collapsible/index.ts new file mode 100644 index 0000000..169b479 --- /dev/null +++ b/web/src/lib/components/ui/collapsible/index.ts @@ -0,0 +1,13 @@ +import Root from "./collapsible.svelte"; +import Trigger from "./collapsible-trigger.svelte"; +import Content from "./collapsible-content.svelte"; + +export { + Root, + Content, + Trigger, + // + Root as Collapsible, + Content as CollapsibleContent, + Trigger as CollapsibleTrigger, +}; diff --git a/web/src/lib/components/ui/dialog/dialog-close.svelte b/web/src/lib/components/ui/dialog/dialog-close.svelte new file mode 100644 index 0000000..de68f2f --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-close.svelte @@ -0,0 +1,11 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-content.svelte b/web/src/lib/components/ui/dialog/dialog-content.svelte new file mode 100644 index 0000000..a4663c9 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-content.svelte @@ -0,0 +1,48 @@ + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/web/src/lib/components/ui/dialog/dialog-description.svelte b/web/src/lib/components/ui/dialog/dialog-description.svelte new file mode 100644 index 0000000..0102d91 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-footer.svelte b/web/src/lib/components/ui/dialog/dialog-footer.svelte new file mode 100644 index 0000000..5685895 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-footer.svelte @@ -0,0 +1,32 @@ + + +
+ {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} +
diff --git a/web/src/lib/components/ui/dialog/dialog-header.svelte b/web/src/lib/components/ui/dialog/dialog-header.svelte new file mode 100644 index 0000000..c3ce8a2 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/dialog/dialog-overlay.svelte b/web/src/lib/components/ui/dialog/dialog-overlay.svelte new file mode 100644 index 0000000..19f69f0 --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-portal.svelte b/web/src/lib/components/ui/dialog/dialog-portal.svelte new file mode 100644 index 0000000..ccfa79c --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-title.svelte b/web/src/lib/components/ui/dialog/dialog-title.svelte new file mode 100644 index 0000000..3f1618b --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog-trigger.svelte b/web/src/lib/components/ui/dialog/dialog-trigger.svelte new file mode 100644 index 0000000..589ee0c --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog-trigger.svelte @@ -0,0 +1,11 @@ + + + diff --git a/web/src/lib/components/ui/dialog/dialog.svelte b/web/src/lib/components/ui/dialog/dialog.svelte new file mode 100644 index 0000000..211672c --- /dev/null +++ b/web/src/lib/components/ui/dialog/dialog.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dialog/index.ts b/web/src/lib/components/ui/dialog/index.ts new file mode 100644 index 0000000..076cef5 --- /dev/null +++ b/web/src/lib/components/ui/dialog/index.ts @@ -0,0 +1,34 @@ +import Root from "./dialog.svelte"; +import Portal from "./dialog-portal.svelte"; +import Title from "./dialog-title.svelte"; +import Footer from "./dialog-footer.svelte"; +import Header from "./dialog-header.svelte"; +import Overlay from "./dialog-overlay.svelte"; +import Content from "./dialog-content.svelte"; +import Description from "./dialog-description.svelte"; +import Trigger from "./dialog-trigger.svelte"; +import Close from "./dialog-close.svelte"; + +export { + Root, + Title, + Portal, + Footer, + Header, + Trigger, + Overlay, + Content, + Description, + Close, + // + Root as Dialog, + Title as DialogTitle, + Portal as DialogPortal, + Footer as DialogFooter, + Header as DialogHeader, + Trigger as DialogTrigger, + Overlay as DialogOverlay, + Content as DialogContent, + Description as DialogDescription, + Close as DialogClose, +}; diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte new file mode 100644 index 0000000..e0e1971 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte new file mode 100644 index 0000000..c04d294 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte @@ -0,0 +1,44 @@ + + + + {#snippet children({ checked, indeterminate })} + + {#if indeterminate} + + {:else if checked} + + {/if} + + {@render childrenProp?.()} + {/snippet} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte new file mode 100644 index 0000000..369bab1 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte @@ -0,0 +1,31 @@ + + + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte new file mode 100644 index 0000000..433540f --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte @@ -0,0 +1,22 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte new file mode 100644 index 0000000..aca1f7b --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte new file mode 100644 index 0000000..0fec5f5 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte @@ -0,0 +1,27 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte new file mode 100644 index 0000000..ad6947a --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte new file mode 100644 index 0000000..274cfef --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte new file mode 100644 index 0000000..189aef4 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte @@ -0,0 +1,16 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte new file mode 100644 index 0000000..d3888cc --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte @@ -0,0 +1,34 @@ + + + + {#snippet children({ checked })} + + {#if checked} + + {/if} + + {@render childrenProp?.({ checked })} + {/snippet} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte new file mode 100644 index 0000000..90f1b6f --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte new file mode 100644 index 0000000..ed7cc85 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte new file mode 100644 index 0000000..254de66 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte new file mode 100644 index 0000000..a390e2d --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte new file mode 100644 index 0000000..f044581 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte new file mode 100644 index 0000000..cb05344 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte b/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte new file mode 100644 index 0000000..cb4bc62 --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/dropdown-menu/index.ts b/web/src/lib/components/ui/dropdown-menu/index.ts new file mode 100644 index 0000000..7850c6a --- /dev/null +++ b/web/src/lib/components/ui/dropdown-menu/index.ts @@ -0,0 +1,54 @@ +import Root from "./dropdown-menu.svelte"; +import Sub from "./dropdown-menu-sub.svelte"; +import CheckboxGroup from "./dropdown-menu-checkbox-group.svelte"; +import CheckboxItem from "./dropdown-menu-checkbox-item.svelte"; +import Content from "./dropdown-menu-content.svelte"; +import Group from "./dropdown-menu-group.svelte"; +import Item from "./dropdown-menu-item.svelte"; +import Label from "./dropdown-menu-label.svelte"; +import RadioGroup from "./dropdown-menu-radio-group.svelte"; +import RadioItem from "./dropdown-menu-radio-item.svelte"; +import Separator from "./dropdown-menu-separator.svelte"; +import Shortcut from "./dropdown-menu-shortcut.svelte"; +import Trigger from "./dropdown-menu-trigger.svelte"; +import SubContent from "./dropdown-menu-sub-content.svelte"; +import SubTrigger from "./dropdown-menu-sub-trigger.svelte"; +import GroupHeading from "./dropdown-menu-group-heading.svelte"; +import Portal from "./dropdown-menu-portal.svelte"; + +export { + CheckboxGroup, + CheckboxItem, + Content, + Portal, + Root as DropdownMenu, + CheckboxGroup as DropdownMenuCheckboxGroup, + CheckboxItem as DropdownMenuCheckboxItem, + Content as DropdownMenuContent, + Portal as DropdownMenuPortal, + Group as DropdownMenuGroup, + Item as DropdownMenuItem, + Label as DropdownMenuLabel, + RadioGroup as DropdownMenuRadioGroup, + RadioItem as DropdownMenuRadioItem, + Separator as DropdownMenuSeparator, + Shortcut as DropdownMenuShortcut, + Sub as DropdownMenuSub, + SubContent as DropdownMenuSubContent, + SubTrigger as DropdownMenuSubTrigger, + Trigger as DropdownMenuTrigger, + GroupHeading as DropdownMenuGroupHeading, + Group, + GroupHeading, + Item, + Label, + RadioGroup, + RadioItem, + Root, + Separator, + Shortcut, + Sub, + SubContent, + SubTrigger, + Trigger, +}; diff --git a/web/src/lib/components/ui/input/index.ts b/web/src/lib/components/ui/input/index.ts new file mode 100644 index 0000000..f47b6d3 --- /dev/null +++ b/web/src/lib/components/ui/input/index.ts @@ -0,0 +1,7 @@ +import Root from "./input.svelte"; + +export { + Root, + // + Root as Input, +}; diff --git a/web/src/lib/components/ui/input/input.svelte b/web/src/lib/components/ui/input/input.svelte new file mode 100644 index 0000000..9978bcb --- /dev/null +++ b/web/src/lib/components/ui/input/input.svelte @@ -0,0 +1,48 @@ + + +{#if type === "file"} + +{:else} + +{/if} diff --git a/web/src/lib/components/ui/label/index.ts b/web/src/lib/components/ui/label/index.ts new file mode 100644 index 0000000..8bfca0b --- /dev/null +++ b/web/src/lib/components/ui/label/index.ts @@ -0,0 +1,7 @@ +import Root from "./label.svelte"; + +export { + Root, + // + Root as Label, +}; diff --git a/web/src/lib/components/ui/label/label.svelte b/web/src/lib/components/ui/label/label.svelte new file mode 100644 index 0000000..d5e3086 --- /dev/null +++ b/web/src/lib/components/ui/label/label.svelte @@ -0,0 +1,20 @@ + + + diff --git a/web/src/lib/components/ui/scroll-area/index.ts b/web/src/lib/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..e86a25b --- /dev/null +++ b/web/src/lib/components/ui/scroll-area/index.ts @@ -0,0 +1,10 @@ +import Scrollbar from "./scroll-area-scrollbar.svelte"; +import Root from "./scroll-area.svelte"; + +export { + Root, + Scrollbar, + //, + Root as ScrollArea, + Scrollbar as ScrollAreaScrollbar, +}; diff --git a/web/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte b/web/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte new file mode 100644 index 0000000..b9518f3 --- /dev/null +++ b/web/src/lib/components/ui/scroll-area/scroll-area-scrollbar.svelte @@ -0,0 +1,30 @@ + + + + {@render children?.()} + + diff --git a/web/src/lib/components/ui/scroll-area/scroll-area.svelte b/web/src/lib/components/ui/scroll-area/scroll-area.svelte new file mode 100644 index 0000000..d4d96d0 --- /dev/null +++ b/web/src/lib/components/ui/scroll-area/scroll-area.svelte @@ -0,0 +1,43 @@ + + + + + {@render children?.()} + + {#if orientation === "vertical" || orientation === "both"} + + {/if} + {#if orientation === "horizontal" || orientation === "both"} + + {/if} + + diff --git a/web/src/lib/components/ui/select/index.ts b/web/src/lib/components/ui/select/index.ts new file mode 100644 index 0000000..4dec358 --- /dev/null +++ b/web/src/lib/components/ui/select/index.ts @@ -0,0 +1,37 @@ +import Root from "./select.svelte"; +import Group from "./select-group.svelte"; +import Label from "./select-label.svelte"; +import Item from "./select-item.svelte"; +import Content from "./select-content.svelte"; +import Trigger from "./select-trigger.svelte"; +import Separator from "./select-separator.svelte"; +import ScrollDownButton from "./select-scroll-down-button.svelte"; +import ScrollUpButton from "./select-scroll-up-button.svelte"; +import GroupHeading from "./select-group-heading.svelte"; +import Portal from "./select-portal.svelte"; + +export { + Root, + Group, + Label, + Item, + Content, + Trigger, + Separator, + ScrollDownButton, + ScrollUpButton, + GroupHeading, + Portal, + // + Root as Select, + Group as SelectGroup, + Label as SelectLabel, + Item as SelectItem, + Content as SelectContent, + Trigger as SelectTrigger, + Separator as SelectSeparator, + ScrollDownButton as SelectScrollDownButton, + ScrollUpButton as SelectScrollUpButton, + GroupHeading as SelectGroupHeading, + Portal as SelectPortal, +}; diff --git a/web/src/lib/components/ui/select/select-content.svelte b/web/src/lib/components/ui/select/select-content.svelte new file mode 100644 index 0000000..887afcd --- /dev/null +++ b/web/src/lib/components/ui/select/select-content.svelte @@ -0,0 +1,45 @@ + + + + + + + {@render children?.()} + + + + diff --git a/web/src/lib/components/ui/select/select-group-heading.svelte b/web/src/lib/components/ui/select/select-group-heading.svelte new file mode 100644 index 0000000..1fab5f0 --- /dev/null +++ b/web/src/lib/components/ui/select/select-group-heading.svelte @@ -0,0 +1,21 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/select/select-group.svelte b/web/src/lib/components/ui/select/select-group.svelte new file mode 100644 index 0000000..f666cb2 --- /dev/null +++ b/web/src/lib/components/ui/select/select-group.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/select/select-item.svelte b/web/src/lib/components/ui/select/select-item.svelte new file mode 100644 index 0000000..29b74f4 --- /dev/null +++ b/web/src/lib/components/ui/select/select-item.svelte @@ -0,0 +1,40 @@ + + + + {#snippet children({ selected, highlighted })} + + {#if selected} + + {/if} + + + {#if childrenProp} + {@render childrenProp({ selected, highlighted })} + {:else} + {label || value} + {/if} + + {/snippet} + diff --git a/web/src/lib/components/ui/select/select-label.svelte b/web/src/lib/components/ui/select/select-label.svelte new file mode 100644 index 0000000..4696025 --- /dev/null +++ b/web/src/lib/components/ui/select/select-label.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/select/select-portal.svelte b/web/src/lib/components/ui/select/select-portal.svelte new file mode 100644 index 0000000..424bcdd --- /dev/null +++ b/web/src/lib/components/ui/select/select-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/select/select-scroll-down-button.svelte b/web/src/lib/components/ui/select/select-scroll-down-button.svelte new file mode 100644 index 0000000..94f41cd --- /dev/null +++ b/web/src/lib/components/ui/select/select-scroll-down-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/web/src/lib/components/ui/select/select-scroll-up-button.svelte b/web/src/lib/components/ui/select/select-scroll-up-button.svelte new file mode 100644 index 0000000..035ea09 --- /dev/null +++ b/web/src/lib/components/ui/select/select-scroll-up-button.svelte @@ -0,0 +1,20 @@ + + + + + diff --git a/web/src/lib/components/ui/select/select-separator.svelte b/web/src/lib/components/ui/select/select-separator.svelte new file mode 100644 index 0000000..3b24bab --- /dev/null +++ b/web/src/lib/components/ui/select/select-separator.svelte @@ -0,0 +1,18 @@ + + + diff --git a/web/src/lib/components/ui/select/select-trigger.svelte b/web/src/lib/components/ui/select/select-trigger.svelte new file mode 100644 index 0000000..6ad07c7 --- /dev/null +++ b/web/src/lib/components/ui/select/select-trigger.svelte @@ -0,0 +1,29 @@ + + + + {@render children?.()} + + diff --git a/web/src/lib/components/ui/select/select.svelte b/web/src/lib/components/ui/select/select.svelte new file mode 100644 index 0000000..05eb663 --- /dev/null +++ b/web/src/lib/components/ui/select/select.svelte @@ -0,0 +1,11 @@ + + + diff --git a/web/src/lib/components/ui/separator/index.ts b/web/src/lib/components/ui/separator/index.ts new file mode 100644 index 0000000..82442d2 --- /dev/null +++ b/web/src/lib/components/ui/separator/index.ts @@ -0,0 +1,7 @@ +import Root from "./separator.svelte"; + +export { + Root, + // + Root as Separator, +}; diff --git a/web/src/lib/components/ui/separator/separator.svelte b/web/src/lib/components/ui/separator/separator.svelte new file mode 100644 index 0000000..5fd8a42 --- /dev/null +++ b/web/src/lib/components/ui/separator/separator.svelte @@ -0,0 +1,23 @@ + + + diff --git a/web/src/lib/components/ui/sheet/index.ts b/web/src/lib/components/ui/sheet/index.ts new file mode 100644 index 0000000..28d7da1 --- /dev/null +++ b/web/src/lib/components/ui/sheet/index.ts @@ -0,0 +1,34 @@ +import Root from "./sheet.svelte"; +import Portal from "./sheet-portal.svelte"; +import Trigger from "./sheet-trigger.svelte"; +import Close from "./sheet-close.svelte"; +import Overlay from "./sheet-overlay.svelte"; +import Content from "./sheet-content.svelte"; +import Header from "./sheet-header.svelte"; +import Footer from "./sheet-footer.svelte"; +import Title from "./sheet-title.svelte"; +import Description from "./sheet-description.svelte"; + +export { + Root, + Close, + Trigger, + Portal, + Overlay, + Content, + Header, + Footer, + Title, + Description, + // + Root as Sheet, + Close as SheetClose, + Trigger as SheetTrigger, + Portal as SheetPortal, + Overlay as SheetOverlay, + Content as SheetContent, + Header as SheetHeader, + Footer as SheetFooter, + Title as SheetTitle, + Description as SheetDescription, +}; diff --git a/web/src/lib/components/ui/sheet/sheet-close.svelte b/web/src/lib/components/ui/sheet/sheet-close.svelte new file mode 100644 index 0000000..ae382c1 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-close.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet-content.svelte b/web/src/lib/components/ui/sheet/sheet-content.svelte new file mode 100644 index 0000000..afdea92 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-content.svelte @@ -0,0 +1,55 @@ + + + + + + + + {@render children?.()} + {#if showCloseButton} + + {#snippet child({ props })} + + {/snippet} + + {/if} + + diff --git a/web/src/lib/components/ui/sheet/sheet-description.svelte b/web/src/lib/components/ui/sheet/sheet-description.svelte new file mode 100644 index 0000000..333b17a --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-description.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet-footer.svelte b/web/src/lib/components/ui/sheet/sheet-footer.svelte new file mode 100644 index 0000000..ad9e07b --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sheet/sheet-header.svelte b/web/src/lib/components/ui/sheet/sheet-header.svelte new file mode 100644 index 0000000..6e51470 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-header.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sheet/sheet-overlay.svelte b/web/src/lib/components/ui/sheet/sheet-overlay.svelte new file mode 100644 index 0000000..8d33847 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-overlay.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet-portal.svelte b/web/src/lib/components/ui/sheet/sheet-portal.svelte new file mode 100644 index 0000000..f3085a3 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet-title.svelte b/web/src/lib/components/ui/sheet/sheet-title.svelte new file mode 100644 index 0000000..13d9443 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-title.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet-trigger.svelte b/web/src/lib/components/ui/sheet/sheet-trigger.svelte new file mode 100644 index 0000000..e266975 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/sheet/sheet.svelte b/web/src/lib/components/ui/sheet/sheet.svelte new file mode 100644 index 0000000..5bf9783 --- /dev/null +++ b/web/src/lib/components/ui/sheet/sheet.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/sidebar/constants.ts b/web/src/lib/components/ui/sidebar/constants.ts new file mode 100644 index 0000000..1347e83 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/constants.ts @@ -0,0 +1,6 @@ +export const SIDEBAR_COOKIE_NAME = "sidebar_state"; +export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7; +export const SIDEBAR_WIDTH = "16rem"; +export const SIDEBAR_WIDTH_MOBILE = "18rem"; +export const SIDEBAR_WIDTH_ICON = "3rem"; +export const SIDEBAR_KEYBOARD_SHORTCUT = "b"; diff --git a/web/src/lib/components/ui/sidebar/context.svelte.ts b/web/src/lib/components/ui/sidebar/context.svelte.ts new file mode 100644 index 0000000..15248ad --- /dev/null +++ b/web/src/lib/components/ui/sidebar/context.svelte.ts @@ -0,0 +1,81 @@ +import { IsMobile } from "$lib/hooks/is-mobile.svelte.js"; +import { getContext, setContext } from "svelte"; +import { SIDEBAR_KEYBOARD_SHORTCUT } from "./constants.js"; + +type Getter = () => T; + +export type SidebarStateProps = { + /** + * A getter function that returns the current open state of the sidebar. + * We use a getter function here to support `bind:open` on the `Sidebar.Provider` + * component. + */ + open: Getter; + + /** + * A function that sets the open state of the sidebar. To support `bind:open`, we need + * a source of truth for changing the open state to ensure it will be synced throughout + * the sub-components and any `bind:` references. + */ + setOpen: (open: boolean) => void; +}; + +class SidebarState { + readonly props: SidebarStateProps; + open = $derived.by(() => this.props.open()); + openMobile = $state(false); + setOpen: SidebarStateProps["setOpen"]; + #isMobile: IsMobile; + state = $derived.by(() => (this.open ? "expanded" : "collapsed")); + + constructor(props: SidebarStateProps) { + this.setOpen = props.setOpen; + this.#isMobile = new IsMobile(); + this.props = props; + } + + // Convenience getter for checking if the sidebar is mobile + // without this, we would need to use `sidebar.isMobile.current` everywhere + get isMobile() { + return this.#isMobile.current; + } + + // Event handler to apply to the `` + handleShortcutKeydown = (e: KeyboardEvent) => { + if (e.key === SIDEBAR_KEYBOARD_SHORTCUT && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + } + }; + + setOpenMobile = (value: boolean) => { + this.openMobile = value; + }; + + toggle = () => { + return this.#isMobile.current + ? (this.openMobile = !this.openMobile) + : this.setOpen(!this.open); + }; +} + +const SYMBOL_KEY = "scn-sidebar"; + +/** + * Instantiates a new `SidebarState` instance and sets it in the context. + * + * @param props The constructor props for the `SidebarState` class. + * @returns The `SidebarState` instance. + */ +export function setSidebar(props: SidebarStateProps): SidebarState { + return setContext(Symbol.for(SYMBOL_KEY), new SidebarState(props)); +} + +/** + * Retrieves the `SidebarState` instance from the context. This is a class instance, + * so you cannot destructure it. + * @returns The `SidebarState` instance. + */ +export function useSidebar(): SidebarState { + return getContext(Symbol.for(SYMBOL_KEY)); +} diff --git a/web/src/lib/components/ui/sidebar/index.ts b/web/src/lib/components/ui/sidebar/index.ts new file mode 100644 index 0000000..318a341 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/index.ts @@ -0,0 +1,75 @@ +import { useSidebar } from "./context.svelte.js"; +import Content from "./sidebar-content.svelte"; +import Footer from "./sidebar-footer.svelte"; +import GroupAction from "./sidebar-group-action.svelte"; +import GroupContent from "./sidebar-group-content.svelte"; +import GroupLabel from "./sidebar-group-label.svelte"; +import Group from "./sidebar-group.svelte"; +import Header from "./sidebar-header.svelte"; +import Input from "./sidebar-input.svelte"; +import Inset from "./sidebar-inset.svelte"; +import MenuAction from "./sidebar-menu-action.svelte"; +import MenuBadge from "./sidebar-menu-badge.svelte"; +import MenuButton from "./sidebar-menu-button.svelte"; +import MenuItem from "./sidebar-menu-item.svelte"; +import MenuSkeleton from "./sidebar-menu-skeleton.svelte"; +import MenuSubButton from "./sidebar-menu-sub-button.svelte"; +import MenuSubItem from "./sidebar-menu-sub-item.svelte"; +import MenuSub from "./sidebar-menu-sub.svelte"; +import Menu from "./sidebar-menu.svelte"; +import Provider from "./sidebar-provider.svelte"; +import Rail from "./sidebar-rail.svelte"; +import Separator from "./sidebar-separator.svelte"; +import Trigger from "./sidebar-trigger.svelte"; +import Root from "./sidebar.svelte"; + +export { + Content, + Footer, + Group, + GroupAction, + GroupContent, + GroupLabel, + Header, + Input, + Inset, + Menu, + MenuAction, + MenuBadge, + MenuButton, + MenuItem, + MenuSkeleton, + MenuSub, + MenuSubButton, + MenuSubItem, + Provider, + Rail, + Root, + Separator, + // + Root as Sidebar, + Content as SidebarContent, + Footer as SidebarFooter, + Group as SidebarGroup, + GroupAction as SidebarGroupAction, + GroupContent as SidebarGroupContent, + GroupLabel as SidebarGroupLabel, + Header as SidebarHeader, + Input as SidebarInput, + Inset as SidebarInset, + Menu as SidebarMenu, + MenuAction as SidebarMenuAction, + MenuBadge as SidebarMenuBadge, + MenuButton as SidebarMenuButton, + MenuItem as SidebarMenuItem, + MenuSkeleton as SidebarMenuSkeleton, + MenuSub as SidebarMenuSub, + MenuSubButton as SidebarMenuSubButton, + MenuSubItem as SidebarMenuSubItem, + Provider as SidebarProvider, + Rail as SidebarRail, + Separator as SidebarSeparator, + Trigger as SidebarTrigger, + Trigger, + useSidebar, +}; diff --git a/web/src/lib/components/ui/sidebar/sidebar-content.svelte b/web/src/lib/components/ui/sidebar/sidebar-content.svelte new file mode 100644 index 0000000..c4111aa --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-content.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-footer.svelte b/web/src/lib/components/ui/sidebar/sidebar-footer.svelte new file mode 100644 index 0000000..496c6c0 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-footer.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte b/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte new file mode 100644 index 0000000..1d07b97 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte b/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte new file mode 100644 index 0000000..7835b0e --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte b/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte new file mode 100644 index 0000000..518216c --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte @@ -0,0 +1,33 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} +
+ {@render children?.()} +
+{/if} diff --git a/web/src/lib/components/ui/sidebar/sidebar-group.svelte b/web/src/lib/components/ui/sidebar/sidebar-group.svelte new file mode 100644 index 0000000..b5880d7 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-group.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-header.svelte b/web/src/lib/components/ui/sidebar/sidebar-header.svelte new file mode 100644 index 0000000..b54754a --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-header.svelte @@ -0,0 +1,21 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-input.svelte b/web/src/lib/components/ui/sidebar/sidebar-input.svelte new file mode 100644 index 0000000..19b3666 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-input.svelte @@ -0,0 +1,21 @@ + + + diff --git a/web/src/lib/components/ui/sidebar/sidebar-inset.svelte b/web/src/lib/components/ui/sidebar/sidebar-inset.svelte new file mode 100644 index 0000000..19a787c --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-inset.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte new file mode 100644 index 0000000..26c81ed --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte @@ -0,0 +1,37 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + +{/if} diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte new file mode 100644 index 0000000..6cecdb4 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte @@ -0,0 +1,24 @@ + + +
+ {@render children?.()} +
diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte new file mode 100644 index 0000000..e0c9c25 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte @@ -0,0 +1,102 @@ + + + + +{#snippet Button({ props }: { props?: Record })} + {@const mergedProps = mergeProps(buttonProps, props)} + {#if child} + {@render child({ props: mergedProps })} + {:else} + + {/if} +{/snippet} + +{#if !tooltipContent} + {@render Button({})} +{:else} + + + {#snippet child({ props })} + {@render Button({ props })} + {/snippet} + + + +{/if} diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte new file mode 100644 index 0000000..4db4453 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte new file mode 100644 index 0000000..1e1249b --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte @@ -0,0 +1,36 @@ + + +
    + {#if showIcon} + + {/if} + + {@render children?.()} +
    diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte new file mode 100644 index 0000000..09ff228 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte @@ -0,0 +1,39 @@ + + +{#if child} + {@render child({ props: mergedProps })} +{:else} + + {@render children?.()} + +{/if} diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte new file mode 100644 index 0000000..681d0f1 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte @@ -0,0 +1,21 @@ + + +
  • + {@render children?.()} +
  • diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte new file mode 100644 index 0000000..29614ee --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/web/src/lib/components/ui/sidebar/sidebar-menu.svelte b/web/src/lib/components/ui/sidebar/sidebar-menu.svelte new file mode 100644 index 0000000..44d6870 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-menu.svelte @@ -0,0 +1,21 @@ + + +
      + {@render children?.()} +
    diff --git a/web/src/lib/components/ui/sidebar/sidebar-provider.svelte b/web/src/lib/components/ui/sidebar/sidebar-provider.svelte new file mode 100644 index 0000000..5b0d0aa --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-provider.svelte @@ -0,0 +1,53 @@ + + + + + +
    + {@render children?.()} +
    +
    diff --git a/web/src/lib/components/ui/sidebar/sidebar-rail.svelte b/web/src/lib/components/ui/sidebar/sidebar-rail.svelte new file mode 100644 index 0000000..f65c869 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-rail.svelte @@ -0,0 +1,36 @@ + + + diff --git a/web/src/lib/components/ui/sidebar/sidebar-separator.svelte b/web/src/lib/components/ui/sidebar/sidebar-separator.svelte new file mode 100644 index 0000000..18d791b --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-separator.svelte @@ -0,0 +1,19 @@ + + + diff --git a/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte b/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte new file mode 100644 index 0000000..dd2dc50 --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte @@ -0,0 +1,36 @@ + + + diff --git a/web/src/lib/components/ui/sidebar/sidebar.svelte b/web/src/lib/components/ui/sidebar/sidebar.svelte new file mode 100644 index 0000000..9c90d4f --- /dev/null +++ b/web/src/lib/components/ui/sidebar/sidebar.svelte @@ -0,0 +1,108 @@ + + +{#if collapsible === "none"} +
    + {@render children?.()} +
    +{:else if sidebar.isMobile} + sidebar.openMobile, (v) => sidebar.setOpenMobile(v)} + {...restProps} + > + button]:hidden", + className + )} + style="--sidebar-width: {SIDEBAR_WIDTH_MOBILE};" + {side} + > + + Sidebar + Displays the mobile sidebar. + +
    + {@render children?.()} +
    +
    +
    +{:else} + +{/if} diff --git a/web/src/lib/components/ui/skeleton/index.ts b/web/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/web/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/web/src/lib/components/ui/skeleton/skeleton.svelte b/web/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..1a940ee --- /dev/null +++ b/web/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
    diff --git a/web/src/lib/components/ui/sonner/index.ts b/web/src/lib/components/ui/sonner/index.ts new file mode 100644 index 0000000..1ad9f4a --- /dev/null +++ b/web/src/lib/components/ui/sonner/index.ts @@ -0,0 +1 @@ +export { default as Toaster } from "./sonner.svelte"; diff --git a/web/src/lib/components/ui/sonner/sonner.svelte b/web/src/lib/components/ui/sonner/sonner.svelte new file mode 100644 index 0000000..34b456d --- /dev/null +++ b/web/src/lib/components/ui/sonner/sonner.svelte @@ -0,0 +1,34 @@ + + + + {#snippet loadingIcon()} + + {/snippet} + {#snippet successIcon()} + + {/snippet} + {#snippet errorIcon()} + + {/snippet} + {#snippet infoIcon()} + + {/snippet} + {#snippet warningIcon()} + + {/snippet} + diff --git a/web/src/lib/components/ui/table/index.ts b/web/src/lib/components/ui/table/index.ts new file mode 100644 index 0000000..14695c8 --- /dev/null +++ b/web/src/lib/components/ui/table/index.ts @@ -0,0 +1,28 @@ +import Root from "./table.svelte"; +import Body from "./table-body.svelte"; +import Caption from "./table-caption.svelte"; +import Cell from "./table-cell.svelte"; +import Footer from "./table-footer.svelte"; +import Head from "./table-head.svelte"; +import Header from "./table-header.svelte"; +import Row from "./table-row.svelte"; + +export { + Root, + Body, + Caption, + Cell, + Footer, + Head, + Header, + Row, + // + Root as Table, + Body as TableBody, + Caption as TableCaption, + Cell as TableCell, + Footer as TableFooter, + Head as TableHead, + Header as TableHeader, + Row as TableRow, +}; diff --git a/web/src/lib/components/ui/table/table-body.svelte b/web/src/lib/components/ui/table/table-body.svelte new file mode 100644 index 0000000..935feae --- /dev/null +++ b/web/src/lib/components/ui/table/table-body.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-caption.svelte b/web/src/lib/components/ui/table/table-caption.svelte new file mode 100644 index 0000000..4696cff --- /dev/null +++ b/web/src/lib/components/ui/table/table-caption.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-cell.svelte b/web/src/lib/components/ui/table/table-cell.svelte new file mode 100644 index 0000000..a998bf6 --- /dev/null +++ b/web/src/lib/components/ui/table/table-cell.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-footer.svelte b/web/src/lib/components/ui/table/table-footer.svelte new file mode 100644 index 0000000..b9b14eb --- /dev/null +++ b/web/src/lib/components/ui/table/table-footer.svelte @@ -0,0 +1,20 @@ + + +tr]:last:border-b-0", className)} + {...restProps} +> + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-head.svelte b/web/src/lib/components/ui/table/table-head.svelte new file mode 100644 index 0000000..267c4e0 --- /dev/null +++ b/web/src/lib/components/ui/table/table-head.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-header.svelte b/web/src/lib/components/ui/table/table-header.svelte new file mode 100644 index 0000000..f47d259 --- /dev/null +++ b/web/src/lib/components/ui/table/table-header.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table-row.svelte b/web/src/lib/components/ui/table/table-row.svelte new file mode 100644 index 0000000..90b4e2a --- /dev/null +++ b/web/src/lib/components/ui/table/table-row.svelte @@ -0,0 +1,15 @@ + + + + {@render children?.()} + diff --git a/web/src/lib/components/ui/table/table.svelte b/web/src/lib/components/ui/table/table.svelte new file mode 100644 index 0000000..d95a02e --- /dev/null +++ b/web/src/lib/components/ui/table/table.svelte @@ -0,0 +1,17 @@ + + +
    + + {@render children?.()} +
    +
    diff --git a/web/src/lib/components/ui/tabs/index.ts b/web/src/lib/components/ui/tabs/index.ts new file mode 100644 index 0000000..31267e5 --- /dev/null +++ b/web/src/lib/components/ui/tabs/index.ts @@ -0,0 +1,18 @@ +import Root from "./tabs.svelte"; +import Content from "./tabs-content.svelte"; +import List, { tabsListVariants, type TabsListVariant } from "./tabs-list.svelte"; +import Trigger from "./tabs-trigger.svelte"; + +export { + Root, + Content, + List, + Trigger, + tabsListVariants, + type TabsListVariant, + // + Root as Tabs, + Content as TabsContent, + List as TabsList, + Trigger as TabsTrigger, +}; diff --git a/web/src/lib/components/ui/tabs/tabs-content.svelte b/web/src/lib/components/ui/tabs/tabs-content.svelte new file mode 100644 index 0000000..394ab3e --- /dev/null +++ b/web/src/lib/components/ui/tabs/tabs-content.svelte @@ -0,0 +1,17 @@ + + + diff --git a/web/src/lib/components/ui/tabs/tabs-list.svelte b/web/src/lib/components/ui/tabs/tabs-list.svelte new file mode 100644 index 0000000..020507c --- /dev/null +++ b/web/src/lib/components/ui/tabs/tabs-list.svelte @@ -0,0 +1,40 @@ + + + + + diff --git a/web/src/lib/components/ui/tabs/tabs-trigger.svelte b/web/src/lib/components/ui/tabs/tabs-trigger.svelte new file mode 100644 index 0000000..4ae962a --- /dev/null +++ b/web/src/lib/components/ui/tabs/tabs-trigger.svelte @@ -0,0 +1,23 @@ + + + diff --git a/web/src/lib/components/ui/tabs/tabs.svelte b/web/src/lib/components/ui/tabs/tabs.svelte new file mode 100644 index 0000000..bfb900d --- /dev/null +++ b/web/src/lib/components/ui/tabs/tabs.svelte @@ -0,0 +1,19 @@ + + + diff --git a/web/src/lib/components/ui/textarea/index.ts b/web/src/lib/components/ui/textarea/index.ts new file mode 100644 index 0000000..ace797a --- /dev/null +++ b/web/src/lib/components/ui/textarea/index.ts @@ -0,0 +1,7 @@ +import Root from "./textarea.svelte"; + +export { + Root, + // + Root as Textarea, +}; diff --git a/web/src/lib/components/ui/textarea/textarea.svelte b/web/src/lib/components/ui/textarea/textarea.svelte new file mode 100644 index 0000000..6005697 --- /dev/null +++ b/web/src/lib/components/ui/textarea/textarea.svelte @@ -0,0 +1,23 @@ + + + diff --git a/web/src/lib/components/ui/tooltip/index.ts b/web/src/lib/components/ui/tooltip/index.ts new file mode 100644 index 0000000..1718604 --- /dev/null +++ b/web/src/lib/components/ui/tooltip/index.ts @@ -0,0 +1,19 @@ +import Root from "./tooltip.svelte"; +import Trigger from "./tooltip-trigger.svelte"; +import Content from "./tooltip-content.svelte"; +import Provider from "./tooltip-provider.svelte"; +import Portal from "./tooltip-portal.svelte"; + +export { + Root, + Trigger, + Content, + Provider, + Portal, + // + Root as Tooltip, + Content as TooltipContent, + Trigger as TooltipTrigger, + Provider as TooltipProvider, + Portal as TooltipPortal, +}; diff --git a/web/src/lib/components/ui/tooltip/tooltip-content.svelte b/web/src/lib/components/ui/tooltip/tooltip-content.svelte new file mode 100644 index 0000000..0cf0694 --- /dev/null +++ b/web/src/lib/components/ui/tooltip/tooltip-content.svelte @@ -0,0 +1,52 @@ + + + + + {@render children?.()} + + {#snippet child({ props })} +
    + {/snippet} +
    +
    +
    diff --git a/web/src/lib/components/ui/tooltip/tooltip-portal.svelte b/web/src/lib/components/ui/tooltip/tooltip-portal.svelte new file mode 100644 index 0000000..d234f7d --- /dev/null +++ b/web/src/lib/components/ui/tooltip/tooltip-portal.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/tooltip/tooltip-provider.svelte b/web/src/lib/components/ui/tooltip/tooltip-provider.svelte new file mode 100644 index 0000000..6dba9a6 --- /dev/null +++ b/web/src/lib/components/ui/tooltip/tooltip-provider.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte b/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte new file mode 100644 index 0000000..c4b21fc --- /dev/null +++ b/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/components/ui/tooltip/tooltip.svelte b/web/src/lib/components/ui/tooltip/tooltip.svelte new file mode 100644 index 0000000..03c9a3d --- /dev/null +++ b/web/src/lib/components/ui/tooltip/tooltip.svelte @@ -0,0 +1,7 @@ + + + diff --git a/web/src/lib/hooks/is-mobile.svelte.ts b/web/src/lib/hooks/is-mobile.svelte.ts new file mode 100644 index 0000000..4829c00 --- /dev/null +++ b/web/src/lib/hooks/is-mobile.svelte.ts @@ -0,0 +1,9 @@ +import { MediaQuery } from "svelte/reactivity"; + +const DEFAULT_MOBILE_BREAKPOINT = 768; + +export class IsMobile extends MediaQuery { + constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) { + super(`max-width: ${breakpoint - 1}px`); + } +} diff --git a/web/src/lib/stores/context.ts b/web/src/lib/stores/context.ts new file mode 100644 index 0000000..89dc28b --- /dev/null +++ b/web/src/lib/stores/context.ts @@ -0,0 +1,63 @@ +import { writable, get } from 'svelte/store' +import { fetchDashboardSummary, fetchApprovals, type DashboardSummary, type Approval } from '$lib/api' +import { liveEvents, subscribeEvents, type OikosEvent } from './events' + +// Shared operational context: dashboard summary + pending approvals, +// refreshed on a slow poll and eagerly on relevant SSE events. Ref-counted +// so the poll only runs while something on screen displays it. + +export const summary = writable(null) +export const pendingApprovals = writable([]) + +let refs = 0 +let pollTimer: ReturnType | null = null +let unsubscribeSSE: (() => void) | null = null +let unsubscribeStore: (() => void) | null = null +let lastSeenEventId = 0 + +export async function refreshContext() { + const [s, approvals] = await Promise.all([fetchDashboardSummary(), fetchApprovals('pending')]) + if (s) summary.set(s) + pendingApprovals.set(approvals) +} + +function onEvent(ev: OikosEvent) { + if (ev.id <= lastSeenEventId) return + lastSeenEventId = ev.id + if ( + ev.type.startsWith('approval.') || + ev.type.startsWith('signal.') || + ev.type.startsWith('execution.') || + ev.type === 'health.changed' + ) { + refreshContext() + } +} + +export function subscribeContext(): () => void { + refs++ + if (refs === 1) { + refreshContext() + pollTimer = setInterval(refreshContext, 30000) + unsubscribeSSE = subscribeEvents() + unsubscribeStore = liveEvents.subscribe((events) => { + if (events[0]) onEvent(events[0]) + }) + } + return () => { + refs-- + if (refs === 0) { + if (pollTimer) clearInterval(pollTimer) + pollTimer = null + unsubscribeSSE?.() + unsubscribeSSE = null + unsubscribeStore?.() + unsubscribeStore = null + } + } +} + +export function openSignalCount(s: DashboardSummary | null): number { + if (!s) return 0 + return Object.values(s.signals_by_severity).reduce((a, b) => a + b, 0) +} diff --git a/web/src/lib/stores/events.ts b/web/src/lib/stores/events.ts new file mode 100644 index 0000000..30b9c76 --- /dev/null +++ b/web/src/lib/stores/events.ts @@ -0,0 +1,57 @@ +import { writable } from 'svelte/store' + +export interface OikosEvent { + id: number + ts: string + type: string + entity_id?: string | null + severity: 'info' | 'warning' | 'critical' + source: string + data?: unknown + correlation_id?: string | null +} + +const MAX_BUFFERED = 200 + +export const liveEvents = writable([]) +export const connectionState = writable<'connecting' | 'open' | 'closed'>('connecting') + +let source: EventSource | null = null +let subscriberCount = 0 + +function connect() { + if (source) return + connectionState.set('connecting') + // The browser's EventSource sends Last-Event-ID automatically on reconnect. + source = new EventSource('/api/v1/events/stream') + + source.onopen = () => connectionState.set('open') + + source.onmessage = (ev) => { + try { + const parsed: OikosEvent = JSON.parse(ev.data) + liveEvents.update((events) => [parsed, ...events].slice(0, MAX_BUFFERED)) + } catch { + // skip malformed + } + } + + source.onerror = () => { + connectionState.set('closed') + } +} + +function disconnect() { + source?.close() + source = null +} + +// Reference-counted: the stream stays open as long as at least one page subscribes. +export function subscribeEvents(): () => void { + subscriberCount++ + if (subscriberCount === 1) connect() + return () => { + subscriberCount-- + if (subscriberCount === 0) disconnect() + } +} diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/web/src/pages/Agent.svelte b/web/src/pages/Agent.svelte new file mode 100644 index 0000000..00df97b --- /dev/null +++ b/web/src/pages/Agent.svelte @@ -0,0 +1,134 @@ + + +
    +
    +

    Agent activity

    + {activities.length} entries +
    + +
    + + load()}> + + {typeFilter === 'all' ? 'All types' : typeFilter} + + + All types + Tool call + Reasoning + Decision + MCP query + Escalation + + + +
    + +
    + + + + + Time + Agent + Type + Tool / Entity + Summary + Status + Duration + + + + {#each activities as a (a.id)} + + {new Date(a.ts).toLocaleString()} + {a.agent_id} + {a.activity_type} + + {#if a.tool_name} + {a.tool_name} + {:else if a.entity_id} + {a.entity_id} + {:else} + + {/if} + + {a.input_summary ?? a.output_summary ?? '—'} + + {#if a.success !== undefined && a.success !== null} + {a.success ? 'ok' : 'fail'} + {:else} + + {/if} + + + {#if a.duration_ms} + {(a.duration_ms / 1000).toFixed(1)}s + {:else} + — + {/if} + + + {:else} + + No agent activity yet. + + {/each} + + + +
    +
    diff --git a/web/src/pages/Audit.svelte b/web/src/pages/Audit.svelte new file mode 100644 index 0000000..543112d --- /dev/null +++ b/web/src/pages/Audit.svelte @@ -0,0 +1,131 @@ + + +
    +
    +

    Audit trail

    + {entries.length} entries +
    + +
    + load()}> + + {actorFilter === 'all' ? 'All actors' : actorFilter} + + + All actors + Agent + Operator + System + Scheduler + + + + + +
    + +
    + + + + + Time + Actor + Action + Entity + Method + Code + Correlation + + + + {#each entries as entry (entry.id)} + + {new Date(entry.ts).toLocaleString()} + +
    + {entry.actor_type} + {#if entry.actor_id} + {entry.actor_id} + {/if} +
    +
    + {entry.action} + {entry.entity_id ?? '—'} + + {#if methodBadge(entry.method)} + {methodBadge(entry.method)} + {:else} + + {/if} + + + {#if entry.status_code} + {entry.status_code} + {:else} + + {/if} + + {entry.correlation_id ?? '—'} +
    + {:else} + + No audit entries. + + {/each} +
    +
    +
    +
    +
    diff --git a/web/src/pages/Chat.svelte b/web/src/pages/Chat.svelte index da64c96..57dcf31 100644 --- a/web/src/pages/Chat.svelte +++ b/web/src/pages/Chat.svelte @@ -1,13 +1,32 @@ -
    -
    - {#each $messages as msg (msg.id)} -
    -
    {msg.role === 'user' ? 'You' : 'Nomos'}
    - {#if msg.text} -
    {msg.text}
    - {/if} - {#each msg.tools as tool (tool.id)} -
    -
    - {tool.type === 'tool_use' ? '⚙' : '✓'} - {tool.name} +
    +
    +
    +
    + {#if $messages.length === 0} +
    +
    +

    Nomos

    +

    Your resident operator. Ask about the fleet, or tell it to act.

    - {#if tool.type === 'tool_use' && tool.args} -
    -
    {JSON.stringify(tool.args, null, 2)}
    -
    - {/if} - {#if tool.type === 'tool_result'} -
    - {#if tool.error} -
    {tool.error}
    - {:else} -
    {JSON.stringify(tool.result, null, 2)}
    +
    + {#each suggestions as q} + + {/each} +
    +
    + {/if} + + {#each $messages as msg (msg.id)} +
    + {#if msg.role === 'user'} +
    {msg.text}
    + {:else} +
    + {#each msg.tools as tool (tool.id)} +
    + + {#if tool.type === 'tool_result' && tool.error} + + {:else if tool.type === 'tool_result'} + + {:else} + + {/if} + {tool.name} + {toolSummary(tool.args)} + +
    + {#if tool.args} +
    {JSON.stringify(tool.args, null, 2)}
    + {/if} + {#if tool.type === 'tool_result'} +
    {tool.error ?? JSON.stringify(tool.result, null, 2)}
    + {/if} +
    +
    + {/each} + {#if msg.text} +
    + + {@html render(msg.text)} +
    + {:else if msg.tools.length === 0} +
    + + + +
    {/if}
    {/if}
    {/each} - {#if !msg.text && msg.tools.length === 0 && msg.role === 'assistant'} -
    Thinking
    - {/if} +
    - {/each} -
    +
    + + {#if $error} +
    +
    + {$error} +
    +
    + {/if} + +
    +
    { + e.preventDefault() + submit() + }} + > + - {#if $streaming} - - {:else} - - {/if} -
    diff --git a/web/src/pages/Entities.svelte b/web/src/pages/Entities.svelte new file mode 100644 index 0000000..4f647c7 --- /dev/null +++ b/web/src/pages/Entities.svelte @@ -0,0 +1,122 @@ + + +
    +
    +

    Entities

    + {filtered.length} of {entities.length} +
    + +
    + + + + {typeFilter === 'all' ? 'All types' : typeFilter} + + + All types + {#each types as type} + {type} + {/each} + + +
    + + {#if loading} +
    + {#each Array(8) as _} + + {/each} +
    + {:else} +
    + + + + Slug + Type + Name + State + Updated + + + + {#each filtered as entity (entity.id)} + (location.hash = '#/entity/' + encodeURIComponent(entity.slug))} + > + {entity.slug} + {entity.type} + {entity.name} + + {#if entity.state} + {entity.state} + {:else} + + {/if} + + {new Date(entity.updated_at).toLocaleString()} + + {:else} + + No entities match this filter. + + {/each} + + +
    + {/if} +
    diff --git a/web/src/pages/EntityDetail.svelte b/web/src/pages/EntityDetail.svelte new file mode 100644 index 0000000..97c51b6 --- /dev/null +++ b/web/src/pages/EntityDetail.svelte @@ -0,0 +1,226 @@ + + +
    + {#if loading} + +
    + + +
    + {:else if !entity} +

    Entity "{slug}" not found.

    + {:else} +
    +

    {entity.slug}

    + {entity.type} + {#if entity.state}{entity.state}{/if} +
    + +
    + + + Attributes + + +
    {JSON.stringify(entity.attributes, null, 2)}
    +
    +
    + + + + Relations ({relations.length}) + + + {#each relations as rel} +
    + {rel.source} + —{rel.type}→ + {rel.target} +
    + {:else} +

    No direct relations.

    + {/each} +
    +
    +
    + + {#if metrics.length} + + + Metrics + + + {#each metrics as series (series.metric)} +
    +

    {series.metric} ({series.rollup})

    +
    +
    + {/each} +
    +
    + {/if} + +
    + + + Open signals + + + {#each signals as signal (signal.id)} +
    + {signal.kind} + {signal.severity} +
    + {:else} +

    None.

    + {/each} +
    +
    + + + + Executions + + + {#each executions as execution (execution.id)} +
    + {execution.action} + {execution.status} +
    + {:else} +

    None.

    + {/each} +
    +
    + + + + Knowledge + + + {#each knowledge as hit (hit.id)} +
    + {hit.type}{hit.title} +
    + {:else} +

    None linked.

    + {/each} +
    +
    +
    + + + + Recent events + + + {#each events as ev (ev.id)} +
    + {new Date(ev.ts).toLocaleString()} + {ev.type} +
    + {:else} +

    No events yet.

    + {/each} +
    +
    + {/if} +
    diff --git a/web/src/pages/Events.svelte b/web/src/pages/Events.svelte new file mode 100644 index 0000000..320430e --- /dev/null +++ b/web/src/pages/Events.svelte @@ -0,0 +1,177 @@ + + +
    +
    +

    Live event feed

    + + stream: {$connectionState} + +
    + +
    + + + + + +
    + +
    + + {#if groupByCorrelation && clustered} +
    + {#each clustered as group (group.corr ?? '__none')} + {@const key = group.corr ?? '__none'} + {@const isExpanded = expandedCorrelations.has(key)} + + {#if isExpanded} + {#each group.events as ev (ev.id)} +
    + {new Date(ev.ts).toLocaleTimeString()} + {ev.severity} + {ev.type} + {ev.source} +
    + {/each} + {/if} + {/each} +
    + {:else} + + + + Time + Severity + Type + Source + Correlation + + + + {#each feed as ev (ev.id)} + + {new Date(ev.ts).toLocaleTimeString()} + {ev.severity} + {ev.type} + {ev.source} + {ev.correlation_id ?? '—'} + + {:else} + + No events yet. + + {/each} + + + {/if} +
    +
    +
    diff --git a/web/src/pages/Graph.svelte b/web/src/pages/Graph.svelte new file mode 100644 index 0000000..a1846b8 --- /dev/null +++ b/web/src/pages/Graph.svelte @@ -0,0 +1,506 @@ + + +
    +
    +

    Graph

    + + + + +
    + + {nodes.length} nodes · {links.length} edges{graph?.truncated ? ' · truncated' : ''} · {Math.round(view.k * 100)}% + +
    + +
    + {#if allNodeTypes.length} +
    + Nodes + {#each allNodeTypes as type} + + {/each} +
    + {/if} + {#if allRelTypes.length} +
    + Edges + {#each allRelTypes as type} + + {/each} +
    + {/if} +
    + + {#if loading && !nodes.length} + + {:else} +
    + + + {#each allRelTypes as type} + + + + {/each} + + + + {#each links as link} + {@const s = endpoint(link.source)} + {@const t = endpoint(link.target)} + {#if s?.x != null && t?.x != null && s?.y != null && t?.y != null && activeRelTypes.has(link.type) && visibleNodeIds.has(s.id) && visibleNodeIds.has(t.id)} + {@const vs = linkVisualState(link)} + {@const dx = t.x - s.x} + {@const dy = t.y - s.y} + {@const len = Math.max(Math.hypot(dx, dy), 1)} + {@const tr = nodeRadius(t) + 3} + {@const ex = t.x - (dx / len) * tr} + {@const ey = t.y - (dy / len) * tr} + + {link.type} + + {#if vs.emphasized && view.k >= 0.7} + + {link.type} + + {/if} + {/if} + {/each} + + + {#each nodes as node (node.id)} + {#if node.x != null && node.y != null && visibleNodeIds.has(node.id)} + {@const r = nodeRadius(node)} + {@const op = nodeOpacity(node)} + {@const isFocus = hoveredId === node.id || selected?.id === node.id} + {@const isMatch = matchedIds !== null && matchedIds.has(node.id)} + onNodePointerDown(e, node)} + onpointerenter={() => (hoveredId = node.id)} + onpointerleave={() => (hoveredId = null)} + onkeydown={(e) => e.key === 'Enter' && selectNode(node)} + ondblclick={() => rerootTo(node)} + > + {#if isFocus || isMatch} + + {/if} + + {#if view.k >= 0.8 || isFocus || isMatch || op === 1 && focusIds !== null} + + {node.slug} + + {/if} + + {/if} + {/each} + + + +
    + scroll to zoom · drag background to pan · drag nodes · click to inspect · double-click to re-root +
    +
    + {/if} +
    + + { if (!open) selected = null }}> + + {#if selected} + + {selected.slug} + {selected.type}{selected.state ? ` — ${selected.state}` : ''} + +
    +
    + + +
    +
    +

    Attributes

    +
    {JSON.stringify(selected.attributes, null, 2)}
    +
    +
    +

    Relationships

    +
    + {#each links.filter((l) => endpointId(l.source) === selected!.id || endpointId(l.target) === selected!.id) as link} + {@const sId = endpointId(link.source)} + {@const other = endpoint(sId === selected!.id ? link.target : link.source)} +
    + + {sId === selected!.id ? '→' : '←'} {link.type} + + {other?.slug ?? '—'} +
    + {:else} +

    No relationships in this view.

    + {/each} +
    +
    +
    +

    Blast radius ({blastRadius.length})

    +
    + {#each blastRadius as item (item.entity.id)} +
    + {item.entity.slug} + depth {item.depth} +
    + {:else} +

    No downstream dependents.

    + {/each} +
    +
    +
    + {/if} +
    +
    diff --git a/web/src/pages/Knowledge.svelte b/web/src/pages/Knowledge.svelte new file mode 100644 index 0000000..1b01410 --- /dev/null +++ b/web/src/pages/Knowledge.svelte @@ -0,0 +1,91 @@ + + +
    +

    Knowledge search

    + +
    { + e.preventDefault() + search() + }} + class="flex gap-2" + > +
    + + +
    + +
    + + {#if searched} +

    {results.length} result{results.length === 1 ? '' : 's'}{query ? ` for "${query}"` : ''}

    + {/if} + + +
    + {#each results as hit (hit.id)} + + +
    + {hit.title} + {hit.type} +
    + {#if hit.snippet} + {@html hit.snippet} + {/if} + {#if hit.linked_entities?.length} +
    + {#each hit.linked_entities as slug} + + {/each} +
    + {/if} +
    +
    + {:else} + {#if searched && !loading} +

    No results found.

    + {/if} + {/each} +
    +
    +
    diff --git a/web/src/pages/Ops.svelte b/web/src/pages/Ops.svelte new file mode 100644 index 0000000..4c13dea --- /dev/null +++ b/web/src/pages/Ops.svelte @@ -0,0 +1,203 @@ + + +
    +

    Operations ledger

    + + + + + Approvals {#if pendingApprovals.length}{pendingApprovals.length}{/if} + + Executions + + + +
    + + + + Subject + Action + Risk + Status + Expires + Decision + + + + {#each pendingApprovals as approval (approval.id)} + + {approval.subject ?? '—'} + {approval.action} + {approval.risk_class} + {approval.status} + {new Date(approval.expires_at).toLocaleString()} + + + + + + {:else} + + No pending approvals. + + {/each} + + +
    + + {#if decidedApprovals.length} +

    Recently decided

    +
    + + + {#each decidedApprovals.slice(0, 20) as approval (approval.id)} + + {approval.subject ?? '—'} + {approval.action} + {approval.status} + {approval.decided_at ? new Date(approval.decided_at).toLocaleString() : '—'} + + {/each} + + +
    + {/if} +
    + + +
    + + + + Target + Action + Status + Correlation + Started + Actions + + + + {#each executions as execution (execution.id)} + + {execution.target ?? '—'} + {execution.action} + {execution.status} + {execution.correlation_id} + {execution.started_at ? new Date(execution.started_at).toLocaleString() : '—'} + + {#if ['proposed', 'approved', 'auto_approved', 'executing'].includes(execution.status)} + + {/if} + + + {:else} + + No executions yet. + + {/each} + + +
    +
    +
    +
    diff --git a/web/src/pages/Overview.svelte b/web/src/pages/Overview.svelte new file mode 100644 index 0000000..4ae5531 --- /dev/null +++ b/web/src/pages/Overview.svelte @@ -0,0 +1,173 @@ + + +
    +

    Overview

    + + {#if loading} +
    + {#each Array(4) as _} + + {/each} +
    + {:else if summary} +
    + + + Entities + + {Object.values(summary.entities_by_type).reduce((a, b) => a + b, 0)} + + + + {#each Object.entries(summary.entities_by_type) as [type, count]} + {type}: {count} + {/each} + + + + + + Health + {summary.health.healthy} healthy + + + healthy: {summary.health.healthy} + degraded: {summary.health.degraded} + down: {summary.health.down} + unknown: {summary.health.unknown} + + + + + + Open signals + + {Object.values(summary.signals_by_severity).reduce((a, b) => a + b, 0)} + + + + {#each Object.entries(summary.signals_by_severity) as [severity, count]} + {severity}: {count} + {/each} + + + + + + Pending approvals + {summary.approvals_pending} + + + {#each Object.entries(summary.executions_by_state) as [state, count]} + {state}: {count} + {/each} + + +
    + + {#if degradedTypes.length} + + + Attention needed + + + {#each degradedTypes as t} + {t.label}: {t.count} + {/each} + + + {/if} + + + + Event rate (6h, 5m buckets) + + +
    + {#each summary.event_rate as bucket} +
    + {/each} +
    +
    +
    + {/if} + + + + Live event ticker + + + +
    + {#each $liveEvents as ev (ev.id)} +
    + {ev.severity} + {new Date(ev.ts).toLocaleTimeString()} + {formatEventLabel(ev)} + {ev.source} +
    + {:else} +

    Waiting for events…

    + {/each} +
    +
    +
    +
    +
    diff --git a/web/src/pages/Signals.svelte b/web/src/pages/Signals.svelte new file mode 100644 index 0000000..55870b3 --- /dev/null +++ b/web/src/pages/Signals.svelte @@ -0,0 +1,171 @@ + + +{#snippet signalTable(list: Signal[], showActions: boolean)} +
    + + + + Target + Kind + Severity + State + Occurrences + Last seen + {#if showActions} + Actions + {/if} + + + + {#each list as signal (signal.id)} + + {signal.target ?? '—'} + {signal.kind} + {signal.severity} + {signal.state} + {signal.occurrence_count} + {new Date(signal.last_seen_at).toLocaleString()} + {#if showActions} + + {#if signal.state === 'raised'} + + {/if} + + + + {/if} + + {:else} + + No signals. + + {/each} + + +
    +{/snippet} + +
    +
    +

    Signals

    + + + {severityFilter === 'all' ? 'All severities' : severityFilter} + + + All severities + Critical + Warning + Info + + +
    + + + + + Open {#if open.length}{open.length}{/if} + + Muted + Resolved + + + {@render signalTable(open, true)} + + + {@render signalTable(muted, true)} + + + {@render signalTable(resolved, false)} + + +
    diff --git a/web/tsconfig.json b/web/tsconfig.json index f73c3c8..2fc3563 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -8,6 +8,7 @@ "isolatedModules": true, "skipLibCheck": true, "paths": { + "$lib": ["./src/lib"], "$lib/*": ["./src/lib/*"] } }, diff --git a/web/vite.config.ts b/web/vite.config.ts index b428259..97acc58 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,8 +1,9 @@ import { svelte } from '@sveltejs/vite-plugin-svelte' +import tailwindcss from '@tailwindcss/vite' import { defineConfig } from 'vite' export default defineConfig({ - plugins: [svelte()], + plugins: [tailwindcss(), svelte()], base: '/ui/', resolve: { alias: { $lib: '/src/lib' }