Compare commits

...

45 Commits

Author SHA1 Message Date
Nick Fitzpatrick
e9967a5795 file organization 2026-03-02 09:07:41 -04:00
Nick Fitzpatrick
c468740de8 use cache data in dependency check 2026-02-28 21:25:51 -04:00
Nick Fitzpatrick
d6465fac4b add command to clear workspace data 2026-02-26 08:56:10 -05:00
Aleksandr Lesnenko
13102782a1 build 2026-02-25 18:26:47 -05:00
Nathan Voxland
3700c3d55f Merge remote-tracking branch 'origin/vscode-extension' into vscode-extension 2026-02-25 18:25:29 -05:00
Aleksandr Lesnenko
68f55e8bb9 build 2026-02-25 18:24:49 -05:00
Nathan Voxland
0719fba2e3 Finished merge 2026-02-25 18:24:40 -05:00
Nathan Voxland
ca736a66c1 Merge remote-tracking branch 'origin/vscode-extension' into vscode-extension
# Conflicts:
#	vscode-extension/README.md
#	vscode-extension/package.json
#	vscode-extension/src/extension-context.ts
#	vscode-extension/src/index.ts
#	vscode-extension/src/webview-html.ts
#	vscode-extension/webview/vite.config.ts
2026-02-25 18:16:41 -05:00
Nathan Voxland
fa88ad06be Added "workspace data" section 2026-02-25 18:15:26 -05:00
Aleksandr Lesnenko
80e4068b48 css 2026-02-25 17:28:49 -05:00
Aleksandr Lesnenko
fa94b39701 css 2026-02-25 17:26:54 -05:00
Aleksandr Lesnenko
f1f6022f9a chopped notebook inside extension 2026-02-25 17:18:41 -05:00
Aleksandr Lesnenko
8bf929bc36 wip 2026-02-25 17:18:41 -05:00
Nathan Voxland
00b8842439 Merge remote-tracking branch 'origin/vscode-extension' into vscode-extension 2026-02-25 16:24:46 -05:00
Nathan Voxland
521f58990d Added sidecar mode 2026-02-25 16:24:06 -05:00
Nick Fitzpatrick
e14ea52ae8 adding create new transform action, fixing some native sql transform display 2026-02-25 16:02:33 -05:00
Aleksandr Lesnenko
98ca93c58c prepare for react previews 2026-02-25 14:12:58 -05:00
Nick Fitzpatrick
1acaa07788 adding graph isolation and filtering 2026-02-25 13:15:48 -05:00
Aleksandr Lesnenko
047b551b22 published tables 2026-02-25 12:45:50 -05:00
Nathan Voxland
1dc2d6b529 Add chat tools to the extension 2026-02-25 12:40:41 -05:00
Nathan Voxland
384101b892 Merge remote-tracking branch 'origin/vscode-extension' into vscode-extension 2026-02-25 11:06:22 -05:00
Nathan Voxland
201ed60f78 Allow opening sql and python in normal editor tools 2026-02-25 11:06:15 -05:00
Aleksandr Lesnenko
e968a7ebed collapse 2026-02-25 09:18:04 -05:00
Nathan Voxland
4c896abb7a Added support for running transforms in workspaces
Cleaned up index.ts file
2026-02-25 09:14:56 -05:00
Nathan Voxland
777b1a6d40 Added support for running transforms in workspaces
Cleaned up index.ts file
2026-02-25 00:36:07 -05:00
Nathan Voxland
1bf5266f20 Merge remote-tracking branch 'origin/master' into vscode-extension 2026-02-24 21:53:07 -05:00
Nathan Voxland
aa0c7e22c8 Merge remote-tracking branch 'origin/vscode-extension' into vscode-extension 2026-02-24 21:43:37 -05:00
Nathan Voxland
c310b67421 Support databases in eid transform API 2026-02-24 21:43:22 -05:00
Aleksandr Lesnenko
8035c1cc44 fix 2026-02-24 18:15:29 -05:00
Aleksandr Lesnenko
be17d7ccfa open in metabase 2026-02-24 18:13:22 -05:00
Aleksandr Lesnenko
d62c6fd2bb polish 2026-02-24 17:57:56 -05:00
Aleksandr Lesnenko
437b903e23 wip 2026-02-24 17:44:00 -05:00
Aleksandr Lesnenko
6d2fbaed2b deps check 2026-02-24 16:38:03 -05:00
Aleksandr Lesnenko
7371226402 graph 2026-02-24 16:28:38 -05:00
Aleksandr Lesnenko
7575671093 Nicks path 2026-02-24 16:14:15 -05:00
Aleksandr Lesnenko
fc83596ed4 resolve 2026-02-24 16:13:17 -05:00
Aleksandr Lesnenko
7387609734 tree 2026-02-24 16:08:07 -05:00
Aleksandr Lesnenko
219ac44245 tree 2026-02-24 16:07:55 -05:00
Nathan Voxland
620407a07d Added host and api configs 2026-02-24 15:55:57 -05:00
Aleksandr Lesnenko
a8b8d57786 trees 2026-02-24 15:45:35 -05:00
Aleksandr Lesnenko
57cb759a01 init 2026-02-24 15:31:58 -05:00
Aleksandr Lesnenko
de82845536 init 2026-02-24 15:31:15 -05:00
Nick Fitzpatrick
3afd3f13b9 include missing stuff 2026-02-24 15:05:09 -05:00
Nick Fitzpatrick
54c9596ab5 setup launch command to point to Hackathon-Sync 2026-02-24 14:59:30 -05:00
Aleksandr Lesnenko
a0b8d00c91 extension 2026-02-24 14:11:56 -05:00
182 changed files with 18102 additions and 24 deletions

1
.gitignore vendored
View File

@@ -172,3 +172,4 @@ CLAUDE.local.md
/resources/python-sources/sqlglot-*.dist-info/
__pycache__/
!vscode-extension/.vscode

View File

@@ -66,6 +66,7 @@
[honey.sql :as sql]
[java-time.api :as t]
[malli.dev :as malli-dev]
[metabase.api-routes.sidecar :as sidecar-routes]
[metabase.api.common :as api]
[metabase.app-db.core :as mdb]
[metabase.app-db.env :as mdb.env]
@@ -176,14 +177,28 @@
(when-let [outdated-ids (seq (map :id (deleted-inmem-databases)))]
(t2/delete! :model/Database :id [:in outdated-ids])))
(defn start-sidecar!
"Start Metabase in sidecar mode. Requires MB_SIDECAR_DIR env var to be set
to the serdes export directory path."
[]
(assert config/sidecar-dir
"MB_SIDECAR_DIR env var must be set to the serdes export directory path")
(let [server-routes (server/make-sidecar-routes #'sidecar-routes/routes)
handler (server/make-sidecar-handler server-routes)]
(server/start-web-server! handler))
((requiring-resolve 'metabase.sidecar/init!)))
(defn start!
"Start Metabase"
[]
(server/start-web-server! (server.test-handler/test-handler))
(init!)
(when config/is-dev?
(prune-deleted-inmem-databases!)
(with-out-str (malli-dev/start!))))
(if config/is-sidecar?
(start-sidecar!)
(do
(server/start-web-server! (server.test-handler/test-handler))
(init!)
(when config/is-dev?
(prune-deleted-inmem-databases!)
(with-out-str (malli-dev/start!))))))
(defn stop!
"Stop Metabase"

View File

@@ -99,5 +99,7 @@
;; this handler has cider middlewares installed:
:handler (apply nrepl-server/default-handler
(conj cider-nrepl/cider-middleware 'refactor-nrepl.middleware/wrap-refactor)))))
((requiring-resolve 'dev/start!))
(if @(requiring-resolve 'metabase.config.core/is-sidecar?)
((requiring-resolve 'dev/start-sidecar!))
((requiring-resolve 'dev/start!)))
(deref (promise)))

View File

@@ -23,6 +23,7 @@ const validEntityTypes = [
"segment",
"snippet",
"timeline",
"transform",
"user",
] as const;

View File

@@ -0,0 +1,108 @@
import { useEffect, useMemo } from "react";
import type { WithRouterProps } from "react-router";
import { skipToken } from "metabase/api";
import {
useListDatabaseSchemaTablesQuery,
useListDatabasesQuery,
} from "metabase/api/database";
import { useGetTableQueryMetadataQuery } from "metabase/api/table";
import { NotFound } from "metabase/common/components/ErrorPages";
import { LoadingAndErrorWrapper } from "metabase/common/components/LoadingAndErrorWrapper";
import { dataStudioData } from "metabase/lib/urls/data-studio";
export function CatalogNameRedirect({ location, router }: WithRouterProps) {
const { database, schema, table, field } = location.query;
const { data: databasesResponse, isLoading: isLoadingDatabases } =
useListDatabasesQuery();
const matchedDatabase = useMemo(
() => databasesResponse?.data.find((db) => db.name === database),
[databasesResponse, database],
);
const databaseId = matchedDatabase?.id;
const needsTable = !!table;
const { data: tables, isLoading: isLoadingTables } =
useListDatabaseSchemaTablesQuery(
databaseId != null && needsTable && schema
? { id: databaseId, schema }
: skipToken,
);
const matchedTable = useMemo(
() => tables?.find((t) => t.name === table),
[tables, table],
);
const tableId = matchedTable?.id;
const needsField = !!field;
const { data: tableMetadata, isLoading: isLoadingMetadata } =
useGetTableQueryMetadataQuery(
tableId != null && needsField ? { id: tableId } : skipToken,
);
const matchedField = useMemo(
() => tableMetadata?.fields?.find((f) => f.name === field),
[tableMetadata, field],
);
const isLoading = isLoadingDatabases || isLoadingTables || isLoadingMetadata;
const resolvedUrl = useMemo(() => {
if (!database || databaseId == null) {
return null;
}
if (!schema) {
return dataStudioData({ databaseId });
}
if (!table) {
return dataStudioData({ databaseId, schemaName: schema });
}
if (tableId == null) {
return null;
}
if (!field) {
return dataStudioData({ databaseId, schemaName: schema, tableId });
}
const fieldId = matchedField?.id;
if (fieldId == null) {
return null;
}
return dataStudioData({
databaseId,
schemaName: schema,
tableId,
tab: "field",
fieldId,
});
}, [database, databaseId, schema, table, tableId, field, matchedField]);
const isNotFound =
!isLoading &&
(resolvedUrl == null ||
(database && !matchedDatabase) ||
(needsTable && tables && !matchedTable) ||
(needsField && tableMetadata && !matchedField));
useEffect(() => {
if (!isLoading && resolvedUrl) {
router.replace(resolvedUrl);
}
}, [isLoading, resolvedUrl, router]);
if (isNotFound) {
return <NotFound />;
}
return <LoadingAndErrorWrapper loading={isLoading} />;
}

View File

@@ -11,11 +11,13 @@ import { DataModelSegmentRevisionHistoryPage } from "metabase/data-studio/segmen
import { PLUGIN_DEPENDENCIES } from "metabase/plugins";
import { IsAdmin } from "metabase/route-guards";
import { CatalogNameRedirect } from "./CatalogNameRedirect";
import { DataModel } from "./pages/DataModel";
export function getDataStudioMetadataRoutes() {
return (
<>
<Route path="by-name" component={CatalogNameRedirect} />
<IndexRoute component={DataModel} />
<Route path="database" component={DataModel} />
<Route path="database/:databaseId" component={DataModel} />

View File

@@ -42,8 +42,8 @@ export function ClausePopover({
}
}, [active]);
const content = renderPopover(handleClose);
const hasPopover = content !== null && !disabled;
const content = disabled ? null : renderPopover(handleClose);
const hasPopover = content !== null;
return (
<PreventPopoverExitProvider>

View File

@@ -57,7 +57,7 @@ export const DataStep = ({
return (
<NotebookCell color={color}>
{isOpened || !table || isEmbed ? (
{!readOnly && (isOpened || !table || isEmbed) ? (
<NotebookDataPicker
query={query}
stageIndex={stageIndex}

View File

@@ -40,7 +40,7 @@ export function JoinTablePicker({
return (
<Box aria-label={t`Right table`}>
{isOpened || !table || isEmbed ? (
{!isReadOnly && (isOpened || !table || isEmbed) ? (
<NotebookDataPicker
title={t`Pick data to join`}
query={query}

View File

@@ -0,0 +1,70 @@
// eslint-disable-next-line no-restricted-imports
import { Global, css } from "@emotion/react";
import { configureStore } from "@reduxjs/toolkit";
import { useMemo } from "react";
import { defaultFontFiles } from "metabase/css/core/fonts.styled";
import { MetabaseReduxProvider } from "metabase/lib/redux/custom-context";
import { getMetabaseCssVariables } from "metabase/styled-components/theme/css-variables";
import { Box, ThemeProvider, useMantineTheme } from "metabase/ui";
import type Question from "metabase-lib/v1/Question";
import { NotebookProvider } from "../Notebook/context";
import { NotebookStepList } from "../NotebookStepList";
interface ReadOnlyNotebookProps {
question: Question;
}
const noop = async () => {};
const readOnlyStore = configureStore({
reducer: {
currentUser: () => null,
settings: () => ({ values: { "site-locale": "en" }, loading: false }),
},
middleware: () => [],
});
const fontStyles = css`
:root {
--mb-default-font-family: "Lato", sans-serif;
}
${defaultFontFiles()}
`;
function ReadOnlyNotebookInner({ question }: ReadOnlyNotebookProps) {
const theme = useMantineTheme();
const cssVariables = useMemo(
() => getMetabaseCssVariables({ theme, whitelabelColors: null }),
[theme],
);
return (
<>
<Global styles={cssVariables} />
<Global styles={fontStyles} />
<NotebookProvider>
<Box pos="relative">
<NotebookStepList
question={question}
reportTimezone=""
updateQuestion={noop}
readOnly
/>
</Box>
</NotebookProvider>
</>
);
}
export function ReadOnlyNotebook({ question }: ReadOnlyNotebookProps) {
return (
<MetabaseReduxProvider store={readOnlyStore}>
<ThemeProvider>
<ReadOnlyNotebookInner question={question} />
</ThemeProvider>
</MetabaseReduxProvider>
);
}

View File

@@ -0,0 +1 @@
export { ReadOnlyNotebook } from "./ReadOnlyNotebook";

View File

@@ -13,7 +13,12 @@ import {
type TranslateEntityIdResponse,
useTranslateEntityIdQuery,
} from "./api/entity-id";
type ResourceType = "dashboard" | "collection" | "card" | "dashboard-tab";
type ResourceType =
| "dashboard"
| "collection"
| "card"
| "dashboard-tab"
| "transform";
type ParamType = "param" | "search";
type ParamConfig = {

View File

@@ -4,6 +4,7 @@ import {
PLUGIN_DEPENDENCIES,
PLUGIN_TRANSFORMS_PYTHON,
} from "metabase/plugins";
import { createEntityIdRedirect } from "metabase/routes-stable-id-aware";
import { JobListPage } from "./pages/JobListPage";
import { JobPage } from "./pages/JobPage";
@@ -36,6 +37,18 @@ export function getDataStudioTransformRoutes() {
<Route path="new/query" component={NewQueryTransformPage} />
<Route path="new/native" component={NewNativeTransformPage} />
<Route path="new/card/:cardId" component={NewCardTransformPage} />
<Route
path="entity/:entity_id"
component={createEntityIdRedirect({
parametersToTranslate: [
{
name: "entity_id",
resourceType: "transform",
type: "param",
},
],
})}
/>
<Route path=":transformId" component={TransformQueryPage} />
<Route path=":transformId/edit" component={TransformQueryPage} />
<Route path=":transformId/run" component={TransformRunPage} />

View File

View File

@@ -0,0 +1,116 @@
/* eslint-env node */
/* eslint-disable import/no-commonjs */
const rspack = require("@rspack/core");
const mainConfig = require("./rspack.main.config");
const { CSS_CONFIG } = require("./frontend/build/shared/rspack/css-config");
const ROOT = __dirname;
const ENTRY_PATH = ROOT + "/vscode-extension/scripts/notebook-entry.tsx";
const OUTPUT_PATH = ROOT + "/vscode-extension/vendor";
/** @type {import('@rspack/cli').Configuration} */
const config = {
mode: "production",
context: ROOT,
devtool: false,
entry: ENTRY_PATH,
output: {
path: OUTPUT_PATH,
filename: "notebook-component.esm.js",
library: { type: "module" },
module: true,
clean: false,
},
experiments: {
outputModule: true,
},
resolve: {
...mainConfig.resolve,
alias: {
...mainConfig.resolve.alias,
"ee-plugins": ROOT + "/frontend/src/metabase/plugins/noop",
"ee-overrides": ROOT + "/frontend/src/metabase/lib/noop",
"sdk-ee-plugins": ROOT + "/frontend/src/metabase/plugins/noop",
"embedding-sdk-shared": ROOT + "/frontend/src/embedding-sdk-shared",
},
},
module: {
rules: [
{
test: /\.(tsx?|jsx?)$/,
exclude: /node_modules|cljs/,
use: [
{
loader: "builtin:swc-loader",
options: {
jsc: {
loose: true,
transform: {
react: { runtime: "automatic" },
},
parser: { syntax: "typescript", tsx: true },
experimental: {
plugins: [["@swc/plugin-emotion", { sourceMap: false }]],
},
},
minify: false,
env: { targets: ["defaults"] },
},
},
],
type: "javascript/auto",
},
{
test: /\.css$/,
use: [
"style-loader",
{ loader: "css-loader", options: CSS_CONFIG },
"postcss-loader",
],
type: "javascript/auto",
},
{
test: /\.(svg|png)$/,
type: "asset/resource",
resourceQuery: { not: [/component|source/] },
},
{
test: /\.svg$/,
type: "asset/source",
resourceQuery: /source/,
},
{
test: /\.svg$/i,
issuer: /\.[jt]sx?$/,
resourceQuery: /component/,
use: [{ loader: "@svgr/webpack", options: { ref: true } }],
},
{
test: /\.md$/,
type: "asset/source",
},
],
},
externals: ["react", "react/jsx-runtime", "react-dom", "react-dom/client"],
optimization: {
minimize: false,
},
plugins: [
new rspack.DefinePlugin({
"process.env.NODE_ENV": JSON.stringify("production"),
"process.env.IS_EMBEDDING_SDK": JSON.stringify("false"),
}),
new rspack.optimize.LimitChunkCountPlugin({ maxChunks: 1 }),
],
};
module.exports = config;

1988
serialization-spec.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,72 @@
(ns metabase.api-routes.sidecar
"Limited API route map for local sidecar mode. Only includes routes needed
by local dev tools (VS Code extension, etc.)."
(:require
[clojure.string :as str]
[metabase.api.macros :as api.macros]
[metabase.api.routes.common :as routes.common :refer [+auth]]
[metabase.api.util]
[metabase.api.util.handlers :as handlers]
[metabase.collections-rest.api]
[metabase.dashboards-rest.api]
[metabase.eid-translation.api]
[metabase.measures.api]
[metabase.queries-rest.api]
[metabase.transforms-rest.api.transform]
[metabase.users-rest.api]
[metabase.warehouse-schema-rest.api]
[metabase.warehouses-rest.api]))
(comment metabase.api.util/keep-me
metabase.collections-rest.api/keep-me
metabase.dashboards-rest.api/keep-me
metabase.eid-translation.api/keep-me
metabase.measures.api/keep-me
metabase.queries-rest.api/keep-me
metabase.transforms-rest.api.transform/keep-me
metabase.users-rest.api/keep-me
metabase.warehouse-schema-rest.api/keep-me
metabase.warehouses-rest.api/keep-me)
(defn- ->handler [x]
(cond-> x
(simple-symbol? x) api.macros/ns-handler))
(defn- +auth* [handler] (+auth (->handler handler)))
(defn- +methods
"Wrap handler to only allow the given HTTP methods. Returns 405 for others."
[handler allowed-methods]
(fn [request respond raise]
(if (contains? allowed-methods (:request-method request))
(handler request respond raise)
(respond {:status 405
:headers {"Allow" (str/join ", " (map (comp str/upper-case name) allowed-methods))}
:body "Method Not Allowed"}))))
(defn- +get-only
"Wrap handler to only allow GET requests."
[handler]
(+methods handler #{:get}))
;;; ↓↓↓ KEEP THIS SORTED ↓↓↓
(def ^:private route-map
{"/card" (+get-only (+auth* metabase.queries-rest.api/card-routes))
"/cards" (+get-only (+auth* metabase.queries-rest.api/cards-routes))
"/collection" (+get-only (+auth* 'metabase.collections-rest.api))
"/dashboard" (+get-only (+auth* 'metabase.dashboards-rest.api))
"/database" (+get-only (+auth* 'metabase.warehouses-rest.api))
"/eid-translation" (+get-only 'metabase.eid-translation.api)
"/field" (+get-only (+auth* metabase.warehouse-schema-rest.api/field-routes))
"/measure" (+get-only (+auth* 'metabase.measures.api))
"/table" (+get-only (+auth* metabase.warehouse-schema-rest.api/table-routes))
"/transform" (+get-only (+auth* metabase.transforms-rest.api.transform/routes))
"/transform-job" (+get-only (+auth* metabase.transforms-rest.api.transform/transform-job-routes))
"/transform-tag" (+get-only (+auth* metabase.transforms-rest.api.transform/transform-tag-routes))
"/user" (+get-only (+auth* 'metabase.users-rest.api))
"/util" (+get-only 'metabase.api.util)})
;;; ↑↑↑ KEEP THIS SORTED ↑↑↑
(def ^{:arglists '([request respond raise])} routes
"Ring routes for the sidecar API -- a limited subset of the full Metabase API."
(handlers/route-map-handler route-map))

View File

@@ -72,6 +72,15 @@
(alter-var-root #'app-defaults merge ee-app-defaults)
;; In sidecar mode, force in-memory H2 and default to port 3033
(def ^:private sidecar-app-defaults
{:mb-db-type "h2"
:mb-db-in-memory "true"
:mb-jetty-port "3033"})
(when (some? (not-empty (get env/env :mb-sidecar-dir)))
(alter-var-root #'app-defaults merge sidecar-app-defaults))
(defn config-str
"Retrieve value for a single configuration key. Accepts either a keyword or a string.
@@ -103,6 +112,16 @@
(def ^Boolean is-dev? "Are we running in `dev` mode (i.e. in a REPL or via `clojure -M:run`)?" (= :dev run-mode))
(def ^Boolean is-prod? "Are we running in `prod` mode (i.e. from a JAR)?" (= :prod run-mode))
(def ^Boolean is-test? "Are we running in `test` mode (i.e. via `clojure -X:test`)?" (= :test run-mode))
(def sidecar-dir
"Path to the serdes export directory for sidecar mode, or nil if not in sidecar mode."
(not-empty (config-str :mb-sidecar-dir)))
(def ^Boolean is-sidecar?
"Are we running in sidecar mode? When true, Metabase runs with a minimal
API surface, H2-only app DB, no authentication overhead, and reduced initialization.
Set via the MB_SIDECAR_DIR environment variable (value is the path to a serdes export directory)."
(some? sidecar-dir))
;; In E2E mode, we can customize the token check URL (e.g., use staging license tokens) while still ensuring that core app logic is unaffected.
;; This allows us to run Cypress E2E tests with the production-like behavior.
(def ^Boolean is-e2e? "Are we running Cypress E2E tests against the production-ready code?" (= :e2e run-mode))

View File

@@ -29,6 +29,7 @@
[metabase.server.core :as server]
[metabase.settings.core :as setting]
[metabase.setup.core :as setup]
[metabase.sidecar :as sidecar]
[metabase.startup.core :as startup]
[metabase.system.core :as system]
[metabase.task.core :as task]
@@ -287,5 +288,7 @@
;; run a command like `java --add-opens java.base/java.nio=ALL-UNNAMED -jar metabase.jar migrate release-locks` or
;; `clojure -M:run migrate release-locks`
(run-cmd cmd init! args)
;; with no command line args just start Metabase normally
(start-normally)))
;; with no command line args just start Metabase normally (or in sidecar mode)
(if config/is-sidecar?
(sidecar/start!)
(start-normally))))

View File

@@ -13,13 +13,16 @@
(defn- api-model?
[model]
(isa? (t2/resolve-model model) :hook/entity-id))
(let [resolved (t2/resolve-model model)]
(or (isa? resolved :hook/entity-id)
(= resolved :model/Database))))
;;; This is no longer calculated dynamically so we don't have to load ~20 model namespaces just to figure out which ones
;;; derive from `:hook/entity-id` just to generate Malli schemas. -- Cam
(def ^:private api-name->model
"Map of model names used on the API to their corresponding model. A test makes sure this map stays in sync."
{:action :model/Action
{:database :model/Database
:action :model/Action
:card :model/Card
:collection :model/Collection
:dashboard :model/Dashboard
@@ -78,6 +81,13 @@
(eid-translation.settings/entity-id-translation-counter!
(merge-with + processed-result (eid-translation.settings/entity-id-translation-counter)))))
(defn- select-eid->id
"Look up entity identifiers. Database uses :name; everything else uses :entity_id."
[model eids]
(if (= model :model/Database)
(into {} (t2/select-fn->fn :name :id [model :id :name] :name [:in eids]))
(into {} (t2/select-fn->fn :entity_id :id [model :id :entity_id] :entity_id [:in eids]))))
(mu/defn- entity-ids->id-for-model :- [:sequential [:tuple
;; We want to pass incorrectly formatted entity-ids through here,
;; but this is assumed to be an entity-id:
@@ -86,17 +96,19 @@
"Given a model and a sequence of entity ids on that model, return a pairs of entity-id, id."
[api-name eids]
(let [model (->model api-name) ;; This lookup is safe because we've already validated the api-names
eid->id (into {} (t2/select-fn->fn :entity_id :id [model :id :entity_id] :entity_id [:in eids]))]
eid->id (select-eid->id model eids)]
(mapv (fn entity-id-info [entity-id]
[entity-id (if-let [id (get eid->id entity-id)]
{:id id :type api-name :status :ok}
;; handle errors
(if (mr/validate EntityId entity-id)
{:type api-name
:status :not-found}
{:type api-name
:status :invalid-format
:reason (me/humanize (mr/explain EntityId entity-id))}))])
(if (= model :model/Database)
{:type api-name :status :not-found}
(if (mr/validate EntityId entity-id)
{:type api-name
:status :not-found}
{:type api-name
:status :invalid-format
:reason (me/humanize (mr/explain EntityId entity-id))})))])
eids)))
(defn model->entity-ids->ids

View File

@@ -20,7 +20,8 @@
(p/import-vars
[metabase.server.handler
make-handler]
make-handler
make-sidecar-handler]
[metabase.server.instance
instance
start-web-server!
@@ -29,7 +30,8 @@
[metabase.server.protocols
Respond]
[metabase.server.routes
make-routes]
make-routes
make-sidecar-routes]
[metabase.server.middleware.json
wrap-json-body
wrap-streamed-json-response]

View File

@@ -5,6 +5,7 @@
[metabase.api.macros :as api.macros]
[metabase.config.core :as config]
[metabase.server.middleware.auth :as mw.auth]
[metabase.sidecar.middleware :as mw.sidecar]
[metabase.server.middleware.browser-cookie :as mw.browser-cookie]
[metabase.server.middleware.exceptions :as mw.exceptions]
[metabase.server.middleware.json :as mw.json]
@@ -128,3 +129,50 @@
(if config/is-dev?
(dev-handler server-routes)
(apply-middleware server-routes)))
;;; ------------------------------------------------- Sidecar Handler -------------------------------------------------
(def ^:private sidecar-middleware
"Reduced middleware stack for local sidecar mode. Removes browser cookies, security headers,
session management, static API keys, SSL redirect, and other middleware not needed locally.
Inserts `wrap-sidecar-user` to inject a synthetic superuser into every request."
[#'mw.exceptions/catch-uncaught-exceptions
#'mw.exceptions/catch-api-exceptions
#'mw.log/log-api-call
#'mw.json/wrap-json-body
#'mw.offset-paging/handle-paging
#'mw.json/wrap-streamed-json-response
#'mw.mp-cache/wrap-metadata-provider-cache
#'wrap-keyword-params
#'wrap-params
#'mw.session/bind-current-user
#'mw.sidecar/wrap-sidecar-user
#'mw.settings-cache/wrap-settings-cache-check
#'mw.misc/add-version
#'mw.misc/add-content-type
#'wrap-gzip
#'mw.request-id/wrap-request-id
#'mw.misc/bind-request])
(mu/defn- apply-sidecar-middleware :- ::api.macros/handler
[handler :- ::api.macros/handler]
(reduce
(fn [handler middleware-fn]
(middleware-fn handler))
handler
sidecar-middleware))
(mu/defn make-sidecar-handler :- ::api.macros/handler
"Create the entry point to the Ring HTTP server for sidecar mode."
[server-routes :- ::api.macros/handler]
(if config/is-dev?
;; In dev, support reloading like the normal dev-handler but with sidecar middleware
(let [handler (atom (apply-sidecar-middleware server-routes))]
(doseq [varr (cons #'sidecar-middleware sidecar-middleware)
:when (instance? clojure.lang.IRef varr)]
(add-watch varr ::sidecar-reload (fn [_key _ref _old-state _new-state]
(log/infof "%s changed, rebuilding sidecar handler" varr)
(reset! handler (apply-sidecar-middleware server-routes)))))
(fn sidecar-dev-handler [request respond raise]
(@handler request respond raise)))
(apply-sidecar-middleware server-routes)))

View File

@@ -118,5 +118,26 @@
;; Anything else (e.g. /user/edit_current) should serve up index.html; React app will handle the rest
(GET "*" [] index/index)))
(mu/defn make-sidecar-routes :- ::api.macros/handler
"Create a minimal top-level Ring route handler for sidecar mode.
Only includes /api/ routes and the health endpoint -- no index pages,
static files, public/embed routes, or auth wrapper."
[api-routes :- ::api.macros/handler]
#_{:clj-kondo/ignore [:discouraged-var]}
(compojure/routes
(GET "/" []
{:status 200
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (str "<!DOCTYPE html><html><head><title>Metabase Sidecar</title></head>"
"<body><h1>Metabase is running in sidecar mode</h1>"
"<p>API routes are available under <code>/api/</code>.</p>"
"</body></html>")})
(GET "/api/health" [] health-handler)
(GET "/readyz" [] health-handler)
(GET "/livez" [] livez-handler)
(OPTIONS "/api/*" [] {:status 200 :body ""})
(context "/api" [] (api-handler api-routes))
(route/not-found {:status 404 :body "Not found."})))
;;; TODO -- if anything changes here we should rebuild these routes? We need a version
;;; of [[metabase.server.handler/dev-handler]] for these routes

163
src/metabase/sidecar.clj Normal file
View File

@@ -0,0 +1,163 @@
(ns metabase.sidecar
"Central namespace for sidecar mode. Consolidates sidecar initialization,
shutdown, serdes import, user management, and database syncing."
(:require
[clojure.java.io :as io]
[metabase.api-routes.sidecar :as sidecar-routes]
[metabase.app-db.core :as mdb]
[metabase.classloader.core :as classloader]
[metabase.config.core :as config]
[metabase.initialization-status.core :as init-status]
[metabase.plugins.core :as plugins]
[metabase.server.core :as server]
[metabase.settings.core :as setting]
[metabase.sidecar.middleware :as mw.sidecar]
[metabase.sidecar.seed :as sidecar-seed]
[metabase.sidecar.watcher :as sidecar-watcher]
[metabase.util.log :as log]
[toucan2.core :as t2]))
(set! *warn-on-reflection* true)
(defonce ^:private sidecar-watcher-state (atom nil))
(def ^:private import-lock (Object.))
(def ^:private sidecar-user-email "sidecar@metabase.local")
(defn- ensure-sidecar-user!
"Ensure a superuser exists for sidecar mode. Creates one if needed.
Sets the sidecar-user-id atom so the middleware can inject it into requests."
[]
(let [existing-user (t2/select-one :model/User :is_superuser true
{:where [:not= :id config/internal-mb-user-id]})]
(if existing-user
(do
(log/infof "Using existing superuser %s (ID %d) for sidecar mode"
(:email existing-user) (:id existing-user))
(reset! mw.sidecar/sidecar-user-id (:id existing-user)))
(let [new-user (t2/insert-returning-instance!
:model/User
{:email sidecar-user-email
:first_name "Sidecar"
:last_name "User"
:password (str (random-uuid)) ; random password, never used
:is_superuser true})]
(log/infof "Created sidecar superuser %s (ID %d)" sidecar-user-email (:id new-user))
(reset! mw.sidecar/sidecar-user-id (:id new-user))))))
(defn- ensure-sidecar-databases!
"Sync Database records with the sidecar export directory. Creates stub records
for new databases and removes records for databases no longer in the export."
[]
(let [db-dir (io/file config/sidecar-dir "databases")]
(when (.isDirectory db-dir)
(let [export-db-names (into #{}
(comp (filter #(.isDirectory ^java.io.File %))
(map #(.getName ^java.io.File %)))
(.listFiles db-dir))
existing-dbs (t2/select :model/Database)]
;; Create stubs for databases in the export that don't exist in the app DB
(doseq [db-name export-db-names]
(when-not (some #(= db-name (:name %)) existing-dbs)
(log/infof "Creating stub database '%s' for sidecar mode" db-name)
(t2/insert! :model/Database
{:name db-name
:engine :h2
:details {}
:initial_sync_status "complete"})))
;; Remove databases no longer in the export directory
(doseq [db existing-dbs]
(when-not (contains? export-db-names (:name db))
(log/infof "Removing stale database '%s' no longer in sidecar export" (:name db))
(t2/delete! :model/Database :id (:id db))))))))
(defn- import-sidecar-serdes!
"Import serdes data from the sidecar directory into the H2 app DB."
[]
(locking import-lock
(ensure-sidecar-databases!)
(let [dir config/sidecar-dir]
(log/infof "Importing serdes data from %s ..." dir)
(let [v2-load-internal! (requiring-resolve
'metabase-enterprise.serialization.cmd/v2-load-internal!)
result (v2-load-internal! dir
{:backfill? true
:continue-on-error true
:reindex? false}
:token-check? false
:require-initialized-db? false)]
(if (seq (:errors result))
(log/warnf "Serdes import completed with %d errors" (count (:errors result)))
(log/infof "Serdes import completed successfully (%d entities)" (count (:seen result))))))))
(defn- destroy-sidecar!
"Shutdown function for sidecar mode."
[]
(log/info "Metabase Sidecar Shutting Down ...")
(when-let [state @sidecar-watcher-state]
(sidecar-watcher/stop-watcher! state))
(server/stop-web-server!)
(let [timeout-seconds 20]
(mdb/release-migration-locks! timeout-seconds))
(log/info "Metabase Sidecar Shutdown COMPLETE"))
(defn init!
"Minimal initialization for sidecar mode. Skips scheduler, sample data, Prometheus,
audit DB, embedding settings, cloud migration, notification seeding, and other
heavyweight startup steps."
[]
#_{:clj-kondo/ignore [:discouraged-var]}
(println (str "\n"
"==============================================\n"
" !! SIDECAR MODE !! \n"
"\n"
" Data Dir: " config/sidecar-dir "\n"
" Port: " (config/config-int :mb-jetty-port) "\n"
"==============================================\n"))
(log/infof "Starting Metabase Sidecar version %s ..." config/mb-version-string)
(when (not= :h2 (config/config-kw :mb-db-type))
(log/warn "Sidecar mode only supports H2. Ignoring MB_DB_TYPE and using H2."))
(init-status/set-progress! 0.1)
;; Register shutdown hook
(.addShutdownHook (Runtime/getRuntime) (Thread. ^Runnable destroy-sidecar!))
(init-status/set-progress! 0.2)
;; Install classloader
(classloader/the-classloader)
;; Load plugins (needed for DB drivers)
(plugins/load-plugins!)
(init-status/set-progress! 0.3)
(setting/validate-settings-formatting!)
;; Setup & migrate H2 DB
(log/info "Setting up sidecar H2 database...")
(mdb/setup-db! :create-sample-content? false)
(init-status/set-progress! 0.6)
;; Load seed data from CSV files
(sidecar-seed/load-seed-data!)
(init-status/set-progress! 0.7)
;; Create/find the sidecar superuser
(ensure-sidecar-user!)
(init-status/set-progress! 0.8)
;; Import serdes data from the sidecar directory
(import-sidecar-serdes!)
;; Start watching for file changes and re-import on change
(reset! sidecar-watcher-state
(sidecar-watcher/start-watcher! config/sidecar-dir import-sidecar-serdes!))
(init-status/set-progress! 0.9)
(init-status/set-complete!)
(log/info "Metabase Sidecar Initialization COMPLETE"))
(defn start!
"Start Metabase in sidecar mode. Sets up routes, handler, web server, and
runs sidecar initialization."
[]
(try
(let [server-routes (server/make-sidecar-routes #'sidecar-routes/routes)
handler (server/make-sidecar-handler server-routes)]
(server/start-web-server! handler))
(init!)
(when (config/config-bool :mb-jetty-join)
(.join (server/instance)))
(catch Throwable e
(log/error e "Metabase Sidecar Initialization FAILED")
(System/exit 1))))

View File

@@ -0,0 +1,22 @@
(ns metabase.sidecar.middleware
"Middleware for local sidecar mode that injects a synthetic superuser into every
request. This replaces `wrap-session-key` + `wrap-current-user-info` so that the
existing `+auth` checks (which look for `:metabase-user-id`) pass through, and
all downstream `*current-user*` bindings work via `bind-current-user`.")
(def sidecar-user-id
"The user ID of the sidecar superuser. Set during sidecar initialization by
[[metabase.sidecar/ensure-sidecar-user!]]."
(atom nil))
(defn wrap-sidecar-user
"Ring middleware that injects the sidecar superuser into every request.
This satisfies the `:metabase-user-id` check in `+auth` and provides
superuser privileges for all sidecar requests."
[handler]
(fn [request respond raise]
(handler (assoc request
:metabase-user-id @sidecar-user-id
:is-superuser? true
:is-group-manager? false)
respond raise)))

View File

@@ -0,0 +1,67 @@
(ns metabase.sidecar.seed
"Loads CSV files from `resources/sidecar/data/` into the H2 application database
on sidecar startup. Each CSV file is loaded into a table whose name matches the
file name (without the `.csv` extension). The first row of each CSV is treated as
column headers."
(:require
[clojure.data.csv :as csv]
[clojure.java.io :as io]
[clojure.java.jdbc :as jdbc]
[clojure.string :as str]
[metabase.app-db.core :as mdb]
[metabase.util.log :as log]))
(set! *warn-on-reflection* true)
(defn- csv-resource-files
"Return a seq of [table-name, URL] pairs for all CSV files in `resources/sidecar/data/`."
[]
(when-let [seed-dir (io/resource "sidecar/data")]
(let [dir (io/file seed-dir)]
(when (.isDirectory dir)
(for [^java.io.File f (sort-by #(.getName ^java.io.File %) (.listFiles dir))
:when (and (.isFile f)
(str/ends-with? (.getName f) ".csv"))]
[(str/replace (.getName f) #"\.csv$" "")
f])))))
(defn- read-csv [file]
(with-open [reader (io/reader file)]
(doall (csv/read-csv reader))))
(defn- create-table-and-insert!
"Create a table with the given name and insert all rows from the CSV data.
The first row is treated as column headers. All columns are created as VARCHAR(1024)."
[ds table-name csv-data]
(let [headers (first csv-data)
rows (rest csv-data)
columns (map #(str/replace % #"[^a-zA-Z0-9_]" "_") headers)
col-defs (str/join ", " (map #(str % " VARCHAR(1024)") columns))]
;; Drop existing table if it exists, then create fresh
(jdbc/execute! {:datasource ds} [(str "DROP TABLE IF EXISTS " table-name)])
(jdbc/execute! {:datasource ds} [(str "CREATE TABLE " table-name " (" col-defs ")")])
(when (seq rows)
(let [placeholders (str/join ", " (repeat (count columns) "?"))
insert-sql (str "INSERT INTO " table-name " VALUES (" placeholders ")")]
(jdbc/execute! {:datasource ds}
(into [insert-sql] (first rows)))
(doseq [row (rest rows)]
(jdbc/execute! {:datasource ds}
(into [insert-sql] row)))))
(log/infof "Loaded %d rows into table %s from CSV" (count rows) table-name)))
(defn load-seed-data!
"Load all CSV files from `resources/sidecar/data/` into the application database.
Each file `foo.csv` is loaded into a table called `foo`."
[]
(let [files (csv-resource-files)]
(if (seq files)
(let [ds (mdb/data-source)]
(doseq [[table-name file] files]
(try
(let [csv-data (read-csv file)]
(when (seq csv-data)
(create-table-and-insert! ds table-name csv-data)))
(catch Exception e
(log/errorf e "Failed to load seed data from %s" (.getName ^java.io.File file))))))
(log/info "No sidecar seed CSV files found in resources/sidecar/data/"))))

View File

@@ -0,0 +1,115 @@
(ns metabase.sidecar.watcher
"Watches a directory tree for file changes and triggers a callback after a
debounce period. Used in sidecar mode to re-import serdes data when files
change on disk (e.g. from a `git pull`)."
(:require
[clojure.java.io :as io]
[metabase.util.log :as log])
(:import
(java.io File)
(java.nio.file ClosedWatchServiceException FileSystems Path
StandardWatchEventKinds WatchEvent$Kind WatchService)))
(set! *warn-on-reflection* true)
(defn- register-recursive!
"Register all directories under `root` (inclusive) with `watcher` for
CREATE, MODIFY, and DELETE events."
[^WatchService watcher ^File root]
(doseq [^File dir (filter #(.isDirectory ^File %) (file-seq root))]
(try
(.register (.toPath dir) watcher
(into-array WatchEvent$Kind
[StandardWatchEventKinds/ENTRY_CREATE
StandardWatchEventKinds/ENTRY_MODIFY
StandardWatchEventKinds/ENTRY_DELETE]))
(catch Exception e
(log/debugf e "Failed to register watcher for directory: %s" dir)))))
(defn- watcher-thread
"Blocks on WatchService events, updating `last-change-time` on any file event.
When a new directory is created, registers it with the watcher too."
[^WatchService watcher state-atom ^File root-dir]
(loop []
(when (:running? @state-atom)
(let [key (try
(.take watcher)
(catch ClosedWatchServiceException _
nil)
(catch Exception e
(log/warnf e "Unexpected error in sidecar file watcher")
nil))]
(when key
(try
(doseq [event (.pollEvents key)]
(let [context (.context event)
kind (.kind event)]
;; If a new directory was created, register it
(when (and (= kind StandardWatchEventKinds/ENTRY_CREATE)
(instance? Path context))
(let [^Path dir-path (.watchable key)
child-path (.resolve dir-path ^Path context)
child-file (.toFile child-path)]
(when (.isDirectory child-file)
(register-recursive! watcher child-file))))))
;; Mark that a change happened
(swap! state-atom assoc :last-change-time (System/currentTimeMillis))
(catch Exception e
(log/warnf e "Error processing file watcher events")))
(.reset key)
(recur))))))
(defn- debounce-thread
"Polls `state-atom` every 500ms. When `last-change-time` is set and more than
`debounce-ms` ago, resets it and calls `on-change`."
[state-atom on-change debounce-ms]
(loop []
(when (:running? @state-atom)
(Thread/sleep 500)
(let [{:keys [last-change-time]} @state-atom]
(when (and last-change-time
(> (- (System/currentTimeMillis) ^long last-change-time)
^long debounce-ms))
(swap! state-atom assoc :last-change-time nil)
(try
(on-change)
(catch Exception e
(log/errorf e "Error in sidecar watcher on-change callback")))))
(recur))))
(defn start-watcher!
"Start watching `dir-path` recursively for file changes. Calls `on-change`
after `debounce-ms` (default 2000) of quiet. Returns a state atom that should
be passed to [[stop-watcher!]] to clean up."
([dir-path on-change]
(start-watcher! dir-path on-change 2000))
([dir-path on-change debounce-ms]
(let [root-dir (io/file dir-path)
watcher (.newWatchService (FileSystems/getDefault))
state (atom {:watcher watcher
:running? true
:last-change-time nil})]
(register-recursive! watcher root-dir)
(log/infof "Starting sidecar file watcher on %s (debounce %dms)" dir-path debounce-ms)
;; Watcher thread — blocks on OS-level file events
(doto (Thread. ^Runnable #(watcher-thread watcher state root-dir)
"sidecar-file-watcher")
(.setDaemon true)
(.start))
;; Debounce thread — polls for quiet period then triggers callback
(doto (Thread. ^Runnable #(debounce-thread state on-change debounce-ms)
"sidecar-file-watcher-debounce")
(.setDaemon true)
(.start))
state)))
(defn stop-watcher!
"Stop the file watcher and its debounce thread."
[state-atom]
(log/info "Stopping sidecar file watcher")
(swap! state-atom assoc :running? false)
(when-let [^WatchService watcher (:watcher @state-atom)]
(try
(.close watcher)
(catch Exception e
(log/debugf e "Error closing WatchService")))))

View File

@@ -132,6 +132,20 @@
:timeline [timeline_eid]
:user [core_user_eid]}))))))
(deftest ^:parallel database-translation-test
(testing "Database translation by name"
(mt/with-temp [:model/Database {db-id :id db-name :name} {:name "test-eid-db" :engine :h2}]
(is (= {db-name {:id db-id :type :database :status :ok}}
(eid-translation.util/model->entity-ids->ids {:database [db-name]})))))
(testing "Database not found returns :not-found (never :invalid-format)"
(is (= {"nonexistent-db-name" {:type :database :status :not-found}}
(eid-translation.util/model->entity-ids->ids {:database ["nonexistent-db-name"]}))))
(testing "->id works with :database and :model/Database"
(mt/with-temp [:model/Database {db-id :id db-name :name} {:name "test-eid-db-2" :engine :h2}]
(is (= db-id (eid-translation.util/->id :database db-name)))
(is (= db-id (eid-translation.util/->id :model/Database db-name)))
(is (= db-id (eid-translation.util/->id :database db-id))))))
(deftest ^:parallel missing-entity-translations-test
(is (= {"abcdefghijklmnopqrstu" {:type :card, :status :not-found}}
(eid-translation.util/model->entity-ids->ids {:card ["abcdefghijklmnopqrstu"]}))))

View File

@@ -0,0 +1,29 @@
{
"permissions": {
"allow": [
"Bash(npx tsdown:*)",
"Bash(npm ls:*)",
"Bash(node:*)",
"WebSearch",
"WebFetch(domain:kermanx.com)",
"WebFetch(domain:github.com)",
"WebFetch(domain:hawkticehurst.com)",
"WebFetch(domain:githubnext.com)",
"WebFetch(domain:www.kenmuse.com)",
"WebFetch(domain:code.visualstudio.com)",
"WebFetch(domain:www.eliostruyf.com)",
"WebFetch(domain:dev.to)",
"WebFetch(domain:medium.com)",
"WebFetch(domain:vogella.com)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(pnpm run build:ext)",
"Bash(npx tsc --noEmit)",
"Bash(npx vitest run)",
"Bash(bun run typecheck)",
"Bash(bun run build:ext)",
"Bash(bun run update)",
"Bash(bunx tsc --noEmit)"
]
}
}

15
vscode-extension/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
.cache
.DS_Store
.idea
*.log
*.tgz
*.vsix
coverage
dist
lib-cov
logs
node_modules
temp
src/generated
vendor/
.env

View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"amodio.tsl-problem-matcher",
"emeraldwalk.runonsave"
]
}

16
vscode-extension/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,16 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}",
"/Users/nickfitzpatrick/Projects/Hackathon-Sync"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "npm: build"
}
]
}

49
vscode-extension/.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,49 @@
{
// Auto generate metadata
"emeraldwalk.runonsave": {
"commands": [
{
"match": "package.json",
"isAsync": true,
"cmd": "npm run update"
}
]
},
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off" },
{ "rule": "*-indent", "severity": "off" },
{ "rule": "*-spacing", "severity": "off" },
{ "rule": "*-spaces", "severity": "off" },
{ "rule": "*-order", "severity": "off" },
{ "rule": "*-dangle", "severity": "off" },
{ "rule": "*-newline", "severity": "off" },
{ "rule": "*quotes", "severity": "off" },
{ "rule": "*semi", "severity": "off" }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml"
]
}

26
vscode-extension/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,26 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "dev",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"problemMatcher": [
{
"base": "$tsc-watch",
"background": {
"activeOnStart": true,
"beginsPattern": "Build start",
"endsPattern": "(B|Reb)uild complete|Rebuilt in"
}
}
],
"group": "build"
}
]
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Anthony Fu <https://github.com/antfu>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,53 @@
# ext-name
<a href="https://marketplace.visualstudio.com/items?itemName=antfu.ext-name" target="__blank"><img src="https://img.shields.io/visual-studio-marketplace/v/antfu.ext-name.svg?color=eee&amp;label=VS%20Code%20Marketplace&logo=visual-studio-code" alt="Visual Studio Marketplace Version" /></a>
<a href="https://kermanx.github.io/reactive-vscode/" target="__blank"><img src="https://img.shields.io/badge/made_with-reactive--vscode-%23007ACC?style=flat&labelColor=%23229863" alt="Made with reactive-vscode" /></a>
## Configurations
<!-- configs -->
| Key | Description | Type | Default |
| ----------------- | --------------------- | -------- | ------- |
| `metastudio.host` | Metabase instance URL | `string` | `""` |
<!-- configs -->
## Commands
<!-- commands -->
| Command | Title |
| ----------------------------------------- | ------------------------------------------ |
| `metastudio.setHost` | Metastudio: Set Host URL |
| `metastudio.setApiKey` | Metastudio: Set API Key |
| `metastudio.checkConnection` | Metastudio: Check Metabase Connection |
| `metastudio.showApiKey` | Metastudio: Show API Key |
| `metastudio.clearCredentials` | Metastudio: Clear Credentials |
| `metastudio.clearWorkspaceConfigurations` | Metastudio: Clear Workspace Configurations |
| `metastudio.checkDependencyGraph` | Metastudio: Check Dependency Graph |
| `metastudio.showDependencyGraph` | Metastudio: Show Dependency Graph |
| `metastudio.showTransformPreview` | Metastudio: Show Transform Preview |
| `metastudio.openInMetabase` | Open in Metabase |
| `metastudio.showInDependencyGraph` | Show in Dependency Graph |
| `metastudio.runTransform` | Run Transform |
| `metastudio.createSqlTransform` | Metastudio: New SQL Transform |
| `metastudio.openEmbeddedCode` | Metastudio: Edit Query in Editor |
| `metastudio.showNotebook` | Metastudio: Show Notebook Query |
| `metastudio.showCardPreview` | Metastudio: Show Card Preview |
| `metastudio.viewTableData` | View Data |
| `metastudio.refreshWorkspaceData` | Refresh Workspace Data |
<!-- commands -->
## Sponsors
<p align="center">
<a href="https://cdn.jsdelivr.net/gh/antfu/static/sponsors.svg">
<img src='https://cdn.jsdelivr.net/gh/antfu/static/sponsors.png'/>
</a>
</p>
## License
[MIT](./LICENSE.md) License © 2022 [Anthony Fu](https://github.com/antfu)

2175
vscode-extension/bun.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
// @ts-check
import antfu from '@antfu/eslint-config'
export default antfu(
{
ignores: [
// eslint ignore globs here
],
},
{
rules: {
// overrides
},
},
)

View File

@@ -0,0 +1 @@
Click the button above to test your connection. If successful, you'll see your Metabase user info confirming authentication is working.

View File

@@ -0,0 +1,3 @@
Enter the URL of your Metabase instance, e.g. `http://localhost:3000` or `https://metabase.example.com`.
This is the base URL you use to access Metabase in your browser.

View File

@@ -0,0 +1,7 @@
To generate an API key in Metabase:
1. Go to **Admin** > **Settings** > **Authentication** > **API Keys**
2. Click **Create API Key**
3. Copy the key and paste it here
Your API key is stored securely in your operating system's credential manager.

View File

@@ -0,0 +1,478 @@
{
"publisher": "metabase",
"name": "metastudio",
"displayName": "Metastudio",
"version": "0.1.0",
"private": true,
"packageManager": "bun@1.3.7",
"description": "Metabase Studio extension for VS Code",
"author": "Metabase",
"license": "MIT",
"homepage": "https://github.com/metabase/metabase",
"repository": {
"type": "git",
"url": "https://github.com/metabase/metabase"
},
"bugs": {
"url": "https://github.com/metabase/metabase/issues"
},
"categories": [
"Other"
],
"main": "./dist/index.cjs",
"icon": "res/icon.png",
"files": [
"LICENSE.md",
"dist/*",
"res/*"
],
"engines": {
"vscode": "^1.101.0"
},
"activationEvents": ["onStartupFinished"],
"contributes": {
"commands": [
{
"command": "metastudio.setHost",
"title": "Metastudio: Set Host URL",
"icon": "$(edit)"
},
{
"command": "metastudio.setApiKey",
"title": "Metastudio: Set API Key",
"icon": "$(edit)"
},
{
"command": "metastudio.checkConnection",
"title": "Metastudio: Check Metabase Connection",
"icon": "$(plug)"
},
{
"command": "metastudio.showApiKey",
"title": "Metastudio: Show API Key",
"icon": "$(key)"
},
{
"command": "metastudio.clearCredentials",
"title": "Metastudio: Clear Credentials",
"icon": "$(close)"
},
{
"command": "metastudio.clearWorkspaceConfigurations",
"title": "Metastudio: Clear Workspace Configurations",
"icon": "$(trash)"
},
{
"command": "metastudio.checkDependencyGraph",
"title": "Metastudio: Check Dependency Graph"
},
{
"command": "metastudio.showDependencyGraph",
"title": "Metastudio: Show Dependency Graph"
},
{
"command": "metastudio.showTransformPreview",
"title": "Metastudio: Show Transform Preview"
},
{
"command": "metastudio.openInMetabase",
"title": "Open in Metabase"
},
{
"command": "metastudio.showInDependencyGraph",
"title": "Show in Dependency Graph"
},
{
"command": "metastudio.runTransform",
"title": "Run Transform",
"icon": "$(play)"
},
{
"command": "metastudio.createSqlTransform",
"title": "Metastudio: New SQL Transform",
"icon": "$(add)"
},
{
"command": "metastudio.openEmbeddedCode",
"title": "Metastudio: Edit Query in Editor",
"icon": "$(edit)"
},
{
"command": "metastudio.showNotebook",
"title": "Metastudio: Show Notebook Query"
},
{
"command": "metastudio.showCardPreview",
"title": "Metastudio: Show Card Preview"
},
{
"command": "metastudio.viewTableData",
"title": "View Data",
"icon": "$(table)"
},
{
"command": "metastudio.refreshWorkspaceData",
"title": "Refresh Workspace Data",
"icon": "$(refresh)"
}
],
"walkthroughs": [
{
"id": "metastudio-setup",
"title": "Get Started with Metastudio",
"description": "Connect your Metabase instance to VS Code",
"steps": [
{
"id": "configure-host",
"title": "Configure Metabase Host",
"description": "Set the URL of your Metabase instance.\n\n[Open Settings](command:workbench.action.openSettings?%5B%22metastudio.host%22%5D)",
"media": {
"markdown": "media/configure-host.md"
},
"completionEvents": [
"onSettingChanged:metastudio.host"
]
},
{
"id": "set-api-key",
"title": "Set API Key",
"description": "Securely store your Metabase API key.\n\n[Set API Key](command:metastudio.setApiKey)",
"media": {
"markdown": "media/set-api-key.md"
},
"completionEvents": [
"onCommand:metastudio.setApiKey"
]
},
{
"id": "check-connection",
"title": "Check Connection",
"description": "Verify that Metastudio can connect to your Metabase instance.\n\n[Check Connection](command:metastudio.checkConnection)",
"media": {
"markdown": "media/check-connection.md"
},
"completionEvents": [
"onCommand:metastudio.checkConnection"
]
}
]
}
],
"viewsContainers": {
"activitybar": [
{
"id": "metabase",
"title": "Metabase",
"icon": "res/sidebar-icon.svg"
}
]
},
"views": {
"metabase": [
{
"id": "metabase.connection",
"name": "Connection",
"visibility": "collapsed"
},
{
"id": "metabase.content",
"name": "Content"
},
{
"id": "metabase.dataCatalog",
"name": "Data Catalog"
},
{
"id": "metabase.workspaceData",
"name": "Workspace Data",
"visibility": "collapsed"
}
]
},
"menus": {
"view/title": [
{
"command": "metastudio.checkConnection",
"when": "view == metabase.connection",
"group": "navigation"
},
{
"command": "metastudio.refreshWorkspaceData",
"when": "view == metabase.workspaceData",
"group": "navigation"
}
],
"view/item/context": [
{
"command": "metastudio.runTransform",
"when": "viewItem == transform",
"group": "inline"
},
{
"command": "metastudio.createSqlTransform",
"when": "viewItem == collectionLibrary",
"group": "inline"
},
{
"command": "metastudio.setHost",
"when": "view == metabase.connection && viewItem == connectionUrl",
"group": "inline@1"
},
{
"command": "metastudio.setApiKey",
"when": "view == metabase.connection && viewItem == connectionToken",
"group": "inline@1"
},
{
"command": "metastudio.clearCredentials",
"when": "view == metabase.connection && viewItem == connectionUrl",
"group": "inline@2"
},
{
"command": "metastudio.clearCredentials",
"when": "view == metabase.connection && viewItem == connectionToken",
"group": "inline@2"
},
{
"command": "metastudio.clearWorkspaceConfigurations",
"when": "view == metabase.connection && viewItem == connectionUrl",
"group": "inline@3"
},
{
"command": "metastudio.openInMetabase",
"when": "view == metabase.content && viewItem =~ /^(card|model|metric|dashboard|collection|collectionLibrary|transform)$/ && metastudio.hostConfigured",
"group": "navigation@1"
},
{
"command": "metastudio.openInMetabase",
"when": "view == metabase.dataCatalog && viewItem =~ /^(database|schema|table|field)$/ && metastudio.hostConfigured",
"group": "navigation@1"
},
{
"command": "metastudio.showNotebook",
"when": "view == metabase.content && viewItem =~ /^(card|model|metric|transform|transformStructured)$/",
"group": "navigation@2"
},
{
"command": "metastudio.showInDependencyGraph",
"when": "view == metabase.content && viewItem =~ /^(card|model|metric|dashboard|collection|collectionLibrary|transform|native_query_snippet|document|action|measure|segment)$/",
"group": "navigation@3"
},
{
"command": "metastudio.showInDependencyGraph",
"when": "view == metabase.dataCatalog && viewItem == table",
"group": "navigation@1"
},
{
"command": "metastudio.viewTableData",
"when": "view == metabase.workspaceData && viewItem == outputTable",
"group": "inline"
},
{
"command": "metastudio.viewTableData",
"when": "view == metabase.workspaceData && viewItem == outputTable",
"group": "navigation@1"
}
]
},
"viewsWelcome": [
{
"view": "metabase.content",
"contents": "Open a folder with Metabase content exported via Remote Sync.",
"when": "!metabase.configExists"
},
{
"view": "metabase.dataCatalog",
"contents": "Open a folder with Metabase content exported via Remote Sync.",
"when": "!metabase.configExists"
}
],
"chatSkills": [
{
"path": "./skills/metastudio/SKILL.md"
}
],
"languageModelTools": [
{
"name": "metastudio_setHost",
"displayName": "Set Metabase Host",
"toolReferenceName": "metastudioSetHost",
"modelDescription": "Sets the Metabase instance URL in workspace configuration. Call this before checking the connection or running transforms.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {
"host": {
"type": "string",
"description": "The full URL of the Metabase instance, e.g. https://my-metabase.example.com"
}
},
"required": ["host"]
}
},
{
"name": "metastudio_setApiKey",
"displayName": "Set or Check Metabase API Key",
"toolReferenceName": "metastudioSetApiKey",
"modelDescription": "Optionally sets the Metabase API key in secure storage, then reports the current key status (whether it is set and its last 4 characters). Omit 'apiKey' to check status without changing anything.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {
"apiKey": {
"type": "string",
"description": "The Metabase API key to store. Omit this property to only check the current status."
}
},
"required": []
}
},
{
"name": "metastudio_checkConnection",
"displayName": "Check Metabase Connection",
"toolReferenceName": "metastudioCheckConnection",
"modelDescription": "Tests connectivity to the configured Metabase instance using the stored API key. Returns the connection result including the authenticated user's name on success.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "metastudio_runTransform",
"displayName": "Run Metabase Transform",
"toolReferenceName": "metastudioRunTransform",
"modelDescription": "Looks up a transform by name (case-insensitive) or by entityId in the loaded content graph and executes it. Provide 'entityId' when multiple transforms share the same name.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The display name of the transform to run (case-insensitive). At least one of 'name' or 'entityId' must be provided."
},
"entityId": {
"type": "string",
"description": "The unique entity ID of the transform to run. Takes precedence over 'name' when both are supplied."
}
},
"required": []
}
},
{
"name": "metastudio_getApiKey",
"displayName": "Get Metabase API Key Status",
"toolReferenceName": "metastudioGetApiKey",
"modelDescription": "Returns the current API key with all but the last 4 characters masked (e.g. ********abcd). Returns 'not set' if no key has been stored.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
},
{
"name": "metastudio_getHost",
"displayName": "Get Metabase Host",
"toolReferenceName": "metastudioGetHost",
"modelDescription": "Returns the currently configured Metabase instance URL, or indicates that no host has been set.",
"canBeReferencedInPrompt": true,
"inputSchema": {
"type": "object",
"properties": {},
"required": []
}
}
],
"configuration": {
"type": "object",
"title": "Metastudio",
"properties": {
"metastudio.host": {
"type": "string",
"format": "uri",
"default": "",
"description": "Metabase instance URL"
}
}
}
},
"scripts": {
"build": "nr build:notebook-component && nr build:ext && nr build:webview",
"build:metabase-lib": "bun run scripts/bundle-metabase-lib.ts",
"build:notebook-component": "test -d ../target/cljs_release || (cd .. && yarn build-release:cljs) ; cd .. && rspack --config rspack.notebook-component.config.js",
"build:ext": "tsdown src/index.ts --external vscode",
"build:webview": "cd webview && vite build",
"dev": "nr build:ext --watch --sourcemap",
"dev:webview": "cd webview && vite",
"prepare": "nr update",
"update": "vscode-ext-gen --output src/generated/meta.ts",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit",
"vscode:prepublish": "nr build",
"release": "bumpp",
"ext:package": "vsce package --no-dependencies",
"ext:publish": "vsxpub --no-dependencies"
},
"devDependencies": {
"@antfu/eslint-config": "^6.7.3",
"@antfu/ni": "^28.0.0",
"@codemirror/autocomplete": "^6.20.0",
"@codemirror/commands": "^6.10.2",
"@codemirror/language": "^6.12.2",
"@codemirror/search": "^6.6.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.39.15",
"@dagrejs/dagre": "^1.1.8",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@lezer/common": "^1.5.1",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.8",
"@mantine/core": "8.3.5",
"@mantine/dates": "8.3.5",
"@mantine/hooks": "8.3.5",
"@reduxjs/toolkit": "^2.11.2",
"@types/node": "^25.0.3",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@types/vscode": "^1.101.0",
"@uiw/react-codemirror": "^4.25.5",
"@vitejs/plugin-react": "^4.4.0",
"@vscode/vsce": "^3.7.1",
"@xyflow/react": "^12.10.1",
"bumpp": "^10.3.2",
"classnames": "^2.5.1",
"dayjs": "^1.11.19",
"esbuild": "^0.25.0",
"eslint": "^9.39.2",
"icepick": "^2.4.0",
"ovsx": "^0.10.7",
"querystring-es3": "^0.2.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-use": "^17.6.0",
"reactive-vscode": "^1.0.0-beta.2",
"remark-gfm": "^4.0.1",
"slugg": "^1.2.1",
"taze": "^19.9.2",
"tsdown": "^0.19.0-beta.3",
"ttag": "^1.8.12",
"typescript": "^5.9.3",
"underscore": "^1.13.8",
"vite": "^7.3.0",
"vitest": "^4.0.16",
"vscode-ext-gen": "^1.5.1",
"vsxpub": "^0.1.2",
"yaml": "^2.8.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.6667 1.66667H24V7H8V9.66667H5.33333V20.3333H8V23H10.6667V28.3333H21.3333V25.6667H26.6667V23H21.3333V20.3333H26.6667V17.6667H21.3333V15H10.6667V20.3333H8V9.66667H24V7H26.6667V1.66667ZM18.6667 25.6667H13.3333V17.6667H18.6667V25.6667Z" fill="#888888"/>
</svg>

After

Width:  |  Height:  |  Size: 366 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-arrow-right-left"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 3 4 4-4 4" />
<path d="M20 7H4" />
<path d="m8 21-4-4 4-4" />
<path d="M4 17h16" />
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.262 2.866A.75.75 0 0 1 5 2.25h5a.75.75 0 0 1 .721.956L9.994 5.75H12.5a.75.75 0 0 1 .55 1.26l-6.5 7a.75.75 0 0 1-1.285-.657l.82-4.103H4a.75.75 0 0 1-.738-.884l1-5.5zm1.364.884l-.727 4H7a.75.75 0 0 1 .735.897l-.48 2.398L10.78 7.25H9a.75.75 0 0 1-.721-.956l.727-2.544h-3.38z" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,26 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-boxes"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" />
<path d="m7 16.5-4.74-2.85" />
<path d="m7 16.5 5-3" />
<path d="M7 16.5v5.17" />
<path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" />
<path d="m17 16.5-5-3" />
<path d="m17 16.5 4.74-2.85" />
<path d="M17 16.5v5.17" />
<path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" />
<path d="M12 8 7.26 5.15" />
<path d="m12 8 4.74-2.85" />
<path d="M12 13.5V8" />
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M5.25 9.5V8h1.5v1.5h-1.5z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 2.25H12c.966 0 1.75.784 1.75 1.75v8A1.75 1.75 0 0 1 12 13.75H4A1.75 1.75 0 0 1 2.25 12V4c0-.966.784-1.75 1.75-1.75h.25V1h1.5v1.25h4.5V1h1.5v1.25zM4 3.75a.25.25 0 0 0-.25.25v1.25h8.5V4a.25.25 0 0 0-.25-.25H4zM3.75 12V6.75h8.5V12a.25.25 0 0 1-.25.25H4a.25.25 0 0 1-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-chart-line"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 3v16a2 2 0 0 0 2 2h16" />
<path d="m19 9-5 5-4-4-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-code"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 18 6-6-6-6" />
<path d="m8 6-6 6 6 6" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M3.309 8.75a4.75 4.75 0 0 0 8.05 2.615.75.75 0 1 0-1.06-1.06 3.25 3.25 0 0 1-2.21.95A.77.77 0 0 0 8 11.25a3.25 3.25 0 0 1-3.25-3.246 3.25 3.25 0 0 1 5.548-2.296.75.75 0 0 0 1.06-1.06 4.75 4.75 0 0 0-3.25-1.39A.757.757 0 0 0 8 3.25a4.75 4.75 0 0 0-4.69 4H2a.75.75 0 0 0 0 1.5h1.309z" /> <path d="M14 8.75a.75.75 0 0 0 0-1.5H9.855a2 2 0 1 0 0 1.5H14z" />
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.75 9a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5zM4 6.25a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.75 5.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M1.25 3c0-.966.784-1.75 1.75-1.75h10c.966 0 1.75.784 1.75 1.75v10A1.75 1.75 0 0 1 13 14.75H3A1.75 1.75 0 0 1 1.25 13V3zm1.5 10V3.5h10.5V13a.25.25 0 0 1-.25.25H3a.25.25 0 0 1-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path clip-rule="evenodd" fill-rule="evenodd" d="M2.665 13.67A.75.75 0 0 1 2.25 13V3a.75.75 0 0 1 .415-.67l.002-.002.002-.001.008-.004.024-.011a7.465 7.465 0 0 1 .38-.164c.255-.103.623-.236 1.088-.37A14.023 14.023 0 0 1 8 1.25c1.583 0 2.903.264 3.831.529.465.133.833.266 1.088.368a7.5 7.5 0 0 1 .38.165l.024.011.008.004.003.001L13 3l.335-.671A.751.751 0 0 1 13.75 3v10a.75.75 0 0 1-.415.67l-.002.002-.002.001-.008.004-.024.011a7.443 7.443 0 0 1-.38.164 11.33 11.33 0 0 1-1.088.37A14.02 14.02 0 0 1 8 14.75a14.02 14.02 0 0 1-3.831-.529 11.327 11.327 0 0 1-1.088-.368 7.409 7.409 0 0 1-.38-.165l-.024-.011-.008-.004-.002-.001-.002-.001zM3.75 3.497c.207-.079.487-.177.831-.275A12.524 12.524 0 0 1 8 2.75c1.417 0 2.597.236 3.419.471.344.098.624.196.831.275v1.24l-.096.038c-.327.133-.642.26-1.106.388-.674.185-1.604.338-3.048.338s-2.374-.153-3.049-.338a8.97 8.97 0 0 1-1.106-.388l-.095-.039V3.496zm7.695 3.113c.289-.08.559-.171.805-.262v2.388l-.096.04c-.327.132-.642.26-1.106.387-.674.185-1.604.338-3.048.338s-2.374-.153-3.049-.338a8.97 8.97 0 0 1-1.201-.427V6.347c.246.091.516.182.805.262C5.378 6.834 6.444 7 8 7c1.556 0 2.621-.166 3.445-.391zM3.75 10.347v2.157c.207.079.487.177.831.275.822.235 2.002.471 3.419.471 1.417 0 2.597-.236 3.419-.471.344-.098.624-.196.831-.275v-2.157c-.246.09-.516.182-.805.261-.824.226-1.89.393-3.445.393-1.556 0-2.622-.167-3.445-.393a9.777 9.777 0 0 1-.805-.26z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg">
<path d="M8.848 1.25c.451 0 .886.175 1.212.487l3.152 3.027c.344.33.538.786.538 1.262V12.8c0 .527-.219 1.026-.597 1.39-.377.362-.882.56-1.403.56h-7.5c-.52 0-1.026-.199-1.403-.56-.331-.318-.54-.74-.587-1.194l-.01-.196V3.2c0-.527.218-1.026.597-1.39.377-.361.882-.56 1.403-.56h4.598zM4.25 2.75a.528.528 0 0 0-.364.143.427.427 0 0 0-.136.307v9.6l.009.082c.017.081.059.16.127.225a.528.528 0 0 0 .364.143h7.5a.528.528 0 0 0 .364-.143.428.428 0 0 0 .136-.307V6.575H9.249A.575.575 0 0 1 8.674 6V2.75H4.25zm6.4 7.425a.576.576 0 0 1 0 1.15H5.4a.575.575 0 0 1 0-1.15h5.25zm0-2.25a.576.576 0 0 1 0 1.15H5.4a.575.575 0 0 1 0-1.15h5.25zm-3.651-2.25a.576.576 0 0 1 0 1.15h-1.6a.575.575 0 0 1 0-1.15H7zm2.825-.25h1.91L9.824 3.59v1.834z" />
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-file-question"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
<path d="M12 17h.01" />
<path d="M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-file-text"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
<path d="M10 9H8" />
<path d="M16 13H8" />
<path d="M16 17H8" />
</svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M1 4.5c0-.69.56-1.25 1.25-1.25h11.5a1.25 1.25 0 1 1 0 2.5H2.25C1.56 5.75 1 5.19 1 4.5zM4 9a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1zm2 4.25a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H7a1 1 0 0 1-1-1z" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.2 3.75a.501.501 0 0 0-.339.126.322.322 0 0 0-.111.235v7.778c0 .076.032.162.111.235a.5.5 0 0 0 .339.126h9.6a.501.501 0 0 0 .339-.126.322.322 0 0 0 .111-.235v-5.96a.322.322 0 0 0-.111-.235.501.501 0 0 0-.339-.126H7.71l-2-1.818H3.2zm-1.358-.975A2 2 0 0 1 3.2 2.25h3.09l2 1.818h4.51a2 2 0 0 1 1.358.525c.371.344.592.823.592 1.336v5.96c0 .513-.22.992-.592 1.336a2 2 0 0 1-1.358.525H3.2a2 2 0 0 1-1.358-.525 1.821 1.821 0 0 1-.592-1.336V4.11c0-.513.22-.992.592-1.336z" />
</svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg">
<path d="M11.41 7.25c.92 0 1.79.185 2.457.519.622.31 1.293.88 1.293 1.731v3c0 .851-.67 1.42-1.293 1.731-.668.334-1.538.519-2.457.519-.92 0-1.79-.185-2.457-.519-.623-.31-1.293-.88-1.293-1.731v-3c0-.851.67-1.42 1.293-1.731.667-.334 1.537-.519 2.457-.519zm-6.096-6a1.986 1.986 0 0 1 1.665.887l.515.762.005.006a.558.558 0 0 0 .464.251h5.038a1.985 1.985 0 0 1 1.985 1.984V6c0 .395-.32.75-.715.75-.395 0-.715-.356-.715-.75v-.86a.555.555 0 0 0-.555-.554H7.962a1.988 1.988 0 0 1-1.658-.894l-.51-.756-.004-.006a.556.556 0 0 0-.47-.25H2.835a.556.556 0 0 0-.555.555v8.26a.558.558 0 0 0 .555.555H5.35c.394 0 .9.32.9.715 0 .394-.505.714-.9.714H2.835A1.986 1.986 0 0 1 .85 11.495V3.235A1.985 1.985 0 0 1 2.835 1.25h2.479zm8.346 10.075c-.636.273-1.423.425-2.25.425-.828 0-1.615-.152-2.25-.425v1.179l.005.014a.327.327 0 0 0 .053.082c.069.08.197.185.406.29.418.209 1.048.36 1.786.36.737 0 1.367-.151 1.786-.36.208-.105.337-.21.405-.29a.329.329 0 0 0 .059-.096v-1.179zM11.41 8.75c-.738 0-1.368.151-1.786.36a1.29 1.29 0 0 0-.406.29.328.328 0 0 0-.058.096v.008l.005.015a.328.328 0 0 0 .053.08c.069.081.197.186.406.29.418.21 1.048.361 1.786.361.737 0 1.367-.151 1.786-.36a1.29 1.29 0 0 0 .405-.29.33.33 0 0 0 .059-.096v-.008A.329.329 0 0 0 13.6 9.4a1.29 1.29 0 0 0-.405-.29c-.418-.209-1.05-.36-1.786-.36z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-hash"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="4" x2="20" y1="9" y2="9" />
<line x1="4" x2="20" y1="15" y2="15" />
<line x1="10" x2="8" y1="3" y2="21" />
<line x1="16" x2="14" y1="3" y2="21" />
</svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M7.245 3.583a.75.75 0 0 0-1.49-.166L5.55 5.25H3.5a.75.75 0 0 0 0 1.5h1.884l-.277 2.5H3.5a.75.75 0 0 0 0 1.5h1.44l-.185 1.667a.75.75 0 1 0 1.49.166l.204-1.833h2.49l-.184 1.667a.75.75 0 1 0 1.49.166l.204-1.833H12.5a.75.75 0 0 0 0-1.5h-1.884l.278-2.5H12.5a.75.75 0 0 0 0-1.5h-1.44l.185-1.667a.75.75 0 0 0-1.49-.166L9.55 5.25h-2.49l.184-1.667zM6.894 6.75l-.278 2.5h2.49l.278-2.5h-2.49z" />
</svg>

After

Width:  |  Height:  |  Size: 542 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-key-round"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
<circle cx="16.5" cy="7.5" r=".5" fill="#C5C5C5" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M11 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M3 3.25A1.75 1.75 0 0 0 1.25 5v6c0 .966.784 1.75 1.75 1.75h7.5a2.75 2.75 0 0 0 2.2-1.1l1.725-2.3c.6-.8.6-1.9 0-2.7L12.7 4.35a2.75 2.75 0 0 0-2.2-1.1H3zM2.75 5A.25.25 0 0 1 3 4.75h7.5c.393 0 .764.185 1 .5l1.725 2.3c.2.267.2.633 0 .9l-1.725 2.3a1.25 1.25 0 0 1-1 .5H3a.25.25 0 0 1-.25-.25V5z" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-layout-dashboard"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="7" height="9" x="3" y="3" rx="1" />
<rect width="7" height="5" x="14" y="3" rx="1" />
<rect width="7" height="9" x="14" y="12" rx="1" />
<rect width="7" height="5" x="3" y="16" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-link"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.562 5.499 12.25 3.81v8.439H3.81L5.5 10.562l.915.915 1.063-1.063-.915-.915.937-.937.915.915 1.063-1.063-.915-.915.937-.937.915.915 1.063-1.063-.915-.915Zm3.188-2.775c0-.935-1.131-1.404-1.793-.742l-9.975 9.976c-.662.661-.193 1.792.742 1.792H13.75V2.724Z" fill="#C5C5C5"/></svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" clip-rule="evenodd" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M8.5 1.423a1 1 0 0 0-1 0L2.554 4.278a1 1 0 0 0-.5.866v5.712a1 1 0 0 0 .5.866L7.5 14.577a1 1 0 0 0 1 0l4.946-2.855a1 1 0 0 0 .5-.866V5.144a1 1 0 0 0-.5-.866L8.5 1.423zM3.554 6.207v4.36l3.696 2.134V8.425L3.554 6.207zM8.75 12.7l3.696-2.134v-4.36L8.75 8.425V12.7zm2.868-7.746L8 2.866 4.382 4.955 8 7.125l3.618-2.17z" />
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-play"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#C5C5C5"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4 3a1 1 0 0 0-1 1v3h6v6h3a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4z" /><path d="M8 8H2v5a1 1 0 0 0 1 1h5V8z" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M1.25 4c0-.966.784-1.75 1.75-1.75a.75.75 0 0 1 0 1.5.25.25 0 0 0-.25.25v2.586c0 .464-.184.909-.513 1.237L2.061 8l.176.177c.329.328.513.773.513 1.237V12c0 .138.112.25.25.25a.75.75 0 0 1 0 1.5A1.75 1.75 0 0 1 1.25 12V9.414a.25.25 0 0 0-.073-.177L.47 8.53a.75.75 0 0 1 0-1.06l.707-.707a.25.25 0 0 0 .073-.177V4zM4.75 11a.75.75 0 0 1 .75-.75H8a.75.75 0 0 1 0 1.5H5.5a.75.75 0 0 1-.75-.75zM4.75 8a.75.75 0 0 1 .75-.75H11a.75.75 0 0 1 0 1.5H5.5A.75.75 0 0 1 4.75 8zM5.5 4.25a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4zM13 13.75A1.75 1.75 0 0 0 14.75 12V9.414a.25.25 0 0 1 .073-.177l.707-.707a.75.75 0 0 0 0-1.06l-.707-.707a.25.25 0 0 1-.073-.177V4A1.75 1.75 0 0 0 13 2.25a.75.75 0 0 0 0 1.5.25.25 0 0 1 .25.25v2.586c0 .464.184.909.513 1.237l.176.177-.176.177a1.75 1.75 0 0 0-.513 1.237V12a.25.25 0 0 1-.25.25.75.75 0 0 0 0 1.5z" />
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M12.667 1.25c1.15 0 2.083.933 2.083 2.083v9.334c0 1.15-.933 2.083-2.083 2.083H3.333a2.083 2.083 0 0 1-2.083-2.083V3.333c0-1.15.933-2.083 2.083-2.083h9.334zm-9.917 9.5v1.917c0 .322.261.583.583.583H7.25v-2.5h-4.5zm6 0v2.5h3.917a.583.583 0 0 0 .583-.583V10.75h-4.5zm-6-1.5h4.5v-2.5h-4.5v2.5zm6 0h4.5v-2.5h-4.5v2.5zm-5.417-6.5a.583.583 0 0 0-.583.583V5.25h4.5v-2.5H3.333zm5.417 2.5h4.5V3.333a.583.583 0 0 0-.583-.583H8.75v2.5z" />
</svg>

After

Width:  |  Height:  |  Size: 564 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#C5C5C5" clip-rule="evenodd" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm1.5 6V7h1.947v2H3.5zm0 1.5v2h1.947v-2H3.5zm3.447 0v2H12.5v-2H6.947zM12.5 9V7H6.947v2H12.5zM5.447 5.5H3.5v-2h1.947v2zm1.5 0H12.5v-2H6.947v2z" />
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#C5C5C5">
<path d="M8.6748 6.83105L12.8584 4.41602L13.5332 5.58398L9.34863 7.99902L13.5332 10.416L12.8584 11.584L8.6748 9.16797V14H7.3252V9.16797L3.1416 11.584L2.4668 10.416L6.65137 7.99902L2.4668 5.58398L3.1416 4.41602L7.3252 6.83105V2H8.6748V6.83105Z"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-arrow-right-left"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 3 4 4-4 4" />
<path d="M20 7H4" />
<path d="m8 21-4-4 4-4" />
<path d="M4 17h16" />
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.262 2.866A.75.75 0 0 1 5 2.25h5a.75.75 0 0 1 .721.956L9.994 5.75H12.5a.75.75 0 0 1 .55 1.26l-6.5 7a.75.75 0 0 1-1.285-.657l.82-4.103H4a.75.75 0 0 1-.738-.884l1-5.5zm1.364.884l-.727 4H7a.75.75 0 0 1 .735.897l-.48 2.398L10.78 7.25H9a.75.75 0 0 1-.721-.956l.727-2.544h-3.38z" />
</svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -0,0 +1,26 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-boxes"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.97 12.92A2 2 0 0 0 2 14.63v3.24a2 2 0 0 0 .97 1.71l3 1.8a2 2 0 0 0 2.06 0L12 19v-5.5l-5-3-4.03 2.42Z" />
<path d="m7 16.5-4.74-2.85" />
<path d="m7 16.5 5-3" />
<path d="M7 16.5v5.17" />
<path d="M12 13.5V19l3.97 2.38a2 2 0 0 0 2.06 0l3-1.8a2 2 0 0 0 .97-1.71v-3.24a2 2 0 0 0-.97-1.71L17 10.5l-5 3Z" />
<path d="m17 16.5-5-3" />
<path d="m17 16.5 4.74-2.85" />
<path d="M17 16.5v5.17" />
<path d="M7.97 4.42A2 2 0 0 0 7 6.13v4.37l5 3 5-3V6.13a2 2 0 0 0-.97-1.71l-3-1.8a2 2 0 0 0-2.06 0l-3 1.8Z" />
<path d="M12 8 7.26 5.15" />
<path d="m12 8 4.74-2.85" />
<path d="M12 13.5V8" />
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M5.25 9.5V8h1.5v1.5h-1.5z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 2.25H12c.966 0 1.75.784 1.75 1.75v8A1.75 1.75 0 0 1 12 13.75H4A1.75 1.75 0 0 1 2.25 12V4c0-.966.784-1.75 1.75-1.75h.25V1h1.5v1.25h4.5V1h1.5v1.25zM4 3.75a.25.25 0 0 0-.25.25v1.25h8.5V4a.25.25 0 0 0-.25-.25H4zM3.75 12V6.75h8.5V12a.25.25 0 0 1-.25.25H4a.25.25 0 0 1-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 498 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-chart-line"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 3v16a2 2 0 0 0 2 2h16" />
<path d="m19 9-5 5-4-4-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-code"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m16 18 6-6-6-6" />
<path d="m8 6-6 6 6 6" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M3.309 8.75a4.75 4.75 0 0 0 8.05 2.615.75.75 0 1 0-1.06-1.06 3.25 3.25 0 0 1-2.21.95A.77.77 0 0 0 8 11.25a3.25 3.25 0 0 1-3.25-3.246 3.25 3.25 0 0 1 5.548-2.296.75.75 0 0 0 1.06-1.06 4.75 4.75 0 0 0-3.25-1.39A.757.757 0 0 0 8 3.25a4.75 4.75 0 0 0-4.69 4H2a.75.75 0 0 0 0 1.5h1.309z" /> <path d="M14 8.75a.75.75 0 0 0 0-1.5H9.855a2 2 0 1 0 0 1.5H14z" />
</svg>

After

Width:  |  Height:  |  Size: 490 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.75 9a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5zM4 6.25a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.75 5.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M1.25 3c0-.966.784-1.75 1.75-1.75h10c.966 0 1.75.784 1.75 1.75v10A1.75 1.75 0 0 1 13 14.75H3A1.75 1.75 0 0 1 1.25 13V3zm1.5 10V3.5h10.5V13a.25.25 0 0 1-.25.25H3a.25.25 0 0 1-.25-.25z" />
</svg>

After

Width:  |  Height:  |  Size: 525 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path clip-rule="evenodd" fill-rule="evenodd" d="M2.665 13.67A.75.75 0 0 1 2.25 13V3a.75.75 0 0 1 .415-.67l.002-.002.002-.001.008-.004.024-.011a7.465 7.465 0 0 1 .38-.164c.255-.103.623-.236 1.088-.37A14.023 14.023 0 0 1 8 1.25c1.583 0 2.903.264 3.831.529.465.133.833.266 1.088.368a7.5 7.5 0 0 1 .38.165l.024.011.008.004.003.001L13 3l.335-.671A.751.751 0 0 1 13.75 3v10a.75.75 0 0 1-.415.67l-.002.002-.002.001-.008.004-.024.011a7.443 7.443 0 0 1-.38.164 11.33 11.33 0 0 1-1.088.37A14.02 14.02 0 0 1 8 14.75a14.02 14.02 0 0 1-3.831-.529 11.327 11.327 0 0 1-1.088-.368 7.409 7.409 0 0 1-.38-.165l-.024-.011-.008-.004-.002-.001-.002-.001zM3.75 3.497c.207-.079.487-.177.831-.275A12.524 12.524 0 0 1 8 2.75c1.417 0 2.597.236 3.419.471.344.098.624.196.831.275v1.24l-.096.038c-.327.133-.642.26-1.106.388-.674.185-1.604.338-3.048.338s-2.374-.153-3.049-.338a8.97 8.97 0 0 1-1.106-.388l-.095-.039V3.496zm7.695 3.113c.289-.08.559-.171.805-.262v2.388l-.096.04c-.327.132-.642.26-1.106.387-.674.185-1.604.338-3.048.338s-2.374-.153-3.049-.338a8.97 8.97 0 0 1-1.201-.427V6.347c.246.091.516.182.805.262C5.378 6.834 6.444 7 8 7c1.556 0 2.621-.166 3.445-.391zM3.75 10.347v2.157c.207.079.487.177.831.275.822.235 2.002.471 3.419.471 1.417 0 2.597-.236 3.419-.471.344-.098.624-.196.831-.275v-2.157c-.246.09-.516.182-.805.261-.824.226-1.89.393-3.445.393-1.556 0-2.622-.167-3.445-.393a9.777 9.777 0 0 1-.805-.26z" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg">
<path d="M8.848 1.25c.451 0 .886.175 1.212.487l3.152 3.027c.344.33.538.786.538 1.262V12.8c0 .527-.219 1.026-.597 1.39-.377.362-.882.56-1.403.56h-7.5c-.52 0-1.026-.199-1.403-.56-.331-.318-.54-.74-.587-1.194l-.01-.196V3.2c0-.527.218-1.026.597-1.39.377-.361.882-.56 1.403-.56h4.598zM4.25 2.75a.528.528 0 0 0-.364.143.427.427 0 0 0-.136.307v9.6l.009.082c.017.081.059.16.127.225a.528.528 0 0 0 .364.143h7.5a.528.528 0 0 0 .364-.143.428.428 0 0 0 .136-.307V6.575H9.249A.575.575 0 0 1 8.674 6V2.75H4.25zm6.4 7.425a.576.576 0 0 1 0 1.15H5.4a.575.575 0 0 1 0-1.15h5.25zm0-2.25a.576.576 0 0 1 0 1.15H5.4a.575.575 0 0 1 0-1.15h5.25zm-3.651-2.25a.576.576 0 0 1 0 1.15h-1.6a.575.575 0 0 1 0-1.15H7zm2.825-.25h1.91L9.824 3.59v1.834z" />
</svg>

After

Width:  |  Height:  |  Size: 809 B

View File

@@ -0,0 +1,17 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-file-question"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
<path d="M12 17h.01" />
<path d="M9.1 9a3 3 0 0 1 5.82 1c0 2-3 3-3 3" />
</svg>

After

Width:  |  Height:  |  Size: 491 B

View File

@@ -0,0 +1,19 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-file-text"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
<path d="M10 9H8" />
<path d="M16 13H8" />
<path d="M16 17H8" />
</svg>

After

Width:  |  Height:  |  Size: 520 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M1 4.5c0-.69.56-1.25 1.25-1.25h11.5a1.25 1.25 0 1 1 0 2.5H2.25C1.56 5.75 1 5.19 1 4.5zM4 9a1 1 0 0 1 1-1h6a1 1 0 1 1 0 2H5a1 1 0 0 1-1-1zm2 4.25a1 1 0 0 1 1-1h2a1 1 0 1 1 0 2H7a1 1 0 0 1-1-1z" />
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.2 3.75a.501.501 0 0 0-.339.126.322.322 0 0 0-.111.235v7.778c0 .076.032.162.111.235a.5.5 0 0 0 .339.126h9.6a.501.501 0 0 0 .339-.126.322.322 0 0 0 .111-.235v-5.96a.322.322 0 0 0-.111-.235.501.501 0 0 0-.339-.126H7.71l-2-1.818H3.2zm-1.358-.975A2 2 0 0 1 3.2 2.25h3.09l2 1.818h4.51a2 2 0 0 1 1.358.525c.371.344.592.823.592 1.336v5.96c0 .513-.22.992-.592 1.336a2 2 0 0 1-1.358.525H3.2a2 2 0 0 1-1.358-.525 1.821 1.821 0 0 1-.592-1.336V4.11c0-.513.22-.992.592-1.336z" />
</svg>

After

Width:  |  Height:  |  Size: 646 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg">
<path d="M11.41 7.25c.92 0 1.79.185 2.457.519.622.31 1.293.88 1.293 1.731v3c0 .851-.67 1.42-1.293 1.731-.668.334-1.538.519-2.457.519-.92 0-1.79-.185-2.457-.519-.623-.31-1.293-.88-1.293-1.731v-3c0-.851.67-1.42 1.293-1.731.667-.334 1.537-.519 2.457-.519zm-6.096-6a1.986 1.986 0 0 1 1.665.887l.515.762.005.006a.558.558 0 0 0 .464.251h5.038a1.985 1.985 0 0 1 1.985 1.984V6c0 .395-.32.75-.715.75-.395 0-.715-.356-.715-.75v-.86a.555.555 0 0 0-.555-.554H7.962a1.988 1.988 0 0 1-1.658-.894l-.51-.756-.004-.006a.556.556 0 0 0-.47-.25H2.835a.556.556 0 0 0-.555.555v8.26a.558.558 0 0 0 .555.555H5.35c.394 0 .9.32.9.715 0 .394-.505.714-.9.714H2.835A1.986 1.986 0 0 1 .85 11.495V3.235A1.985 1.985 0 0 1 2.835 1.25h2.479zm8.346 10.075c-.636.273-1.423.425-2.25.425-.828 0-1.615-.152-2.25-.425v1.179l.005.014a.327.327 0 0 0 .053.082c.069.08.197.185.406.29.418.209 1.048.36 1.786.36.737 0 1.367-.151 1.786-.36.208-.105.337-.21.405-.29a.329.329 0 0 0 .059-.096v-1.179zM11.41 8.75c-.738 0-1.368.151-1.786.36a1.29 1.29 0 0 0-.406.29.328.328 0 0 0-.058.096v.008l.005.015a.328.328 0 0 0 .053.08c.069.081.197.186.406.29.418.21 1.048.361 1.786.361.737 0 1.367-.151 1.786-.36a1.29 1.29 0 0 0 .405-.29.33.33 0 0 0 .059-.096v-.008A.329.329 0 0 0 13.6 9.4a1.29 1.29 0 0 0-.405-.29c-.418-.209-1.05-.36-1.786-.36z" />
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-hash"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="4" x2="20" y1="9" y2="9" />
<line x1="4" x2="20" y1="15" y2="15" />
<line x1="10" x2="8" y1="3" y2="21" />
<line x1="16" x2="14" y1="3" y2="21" />
</svg>

After

Width:  |  Height:  |  Size: 444 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M7.245 3.583a.75.75 0 0 0-1.49-.166L5.55 5.25H3.5a.75.75 0 0 0 0 1.5h1.884l-.277 2.5H3.5a.75.75 0 0 0 0 1.5h1.44l-.185 1.667a.75.75 0 1 0 1.49.166l.204-1.833h2.49l-.184 1.667a.75.75 0 1 0 1.49.166l.204-1.833H12.5a.75.75 0 0 0 0-1.5h-1.884l.278-2.5H12.5a.75.75 0 0 0 0-1.5h-1.44l.185-1.667a.75.75 0 0 0-1.49-.166L9.55 5.25h-2.49l.184-1.667zM6.894 6.75l-.278 2.5h2.49l.278-2.5h-2.49z" />
</svg>

After

Width:  |  Height:  |  Size: 542 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-key-round"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2.586 17.414A2 2 0 0 0 2 18.828V21a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h1a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1h.172a2 2 0 0 0 1.414-.586l.814-.814a6.5 6.5 0 1 0-4-4z" />
<circle cx="16.5" cy="7.5" r=".5" fill="#424242" />
</svg>

After

Width:  |  Height:  |  Size: 524 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M11 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2z" /><path fill-rule="evenodd" clip-rule="evenodd" d="M3 3.25A1.75 1.75 0 0 0 1.25 5v6c0 .966.784 1.75 1.75 1.75h7.5a2.75 2.75 0 0 0 2.2-1.1l1.725-2.3c.6-.8.6-1.9 0-2.7L12.7 4.35a2.75 2.75 0 0 0-2.2-1.1H3zM2.75 5A.25.25 0 0 1 3 4.75h7.5c.393 0 .764.185 1 .5l1.725 2.3c.2.267.2.633 0 .9l-1.725 2.3a1.25 1.25 0 0 1-1 .5H3a.25.25 0 0 1-.25-.25V5z" />
</svg>

After

Width:  |  Height:  |  Size: 518 B

View File

@@ -0,0 +1,18 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-layout-dashboard"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="7" height="9" x="3" y="3" rx="1" />
<rect width="7" height="5" x="14" y="3" rx="1" />
<rect width="7" height="9" x="14" y="12" rx="1" />
<rect width="7" height="5" x="3" y="16" rx="1" />
</svg>

After

Width:  |  Height:  |  Size: 499 B

View File

@@ -0,0 +1,16 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-link"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
</svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.562 5.499 12.25 3.81v8.439H3.81L5.5 10.562l.915.915 1.063-1.063-.915-.915.937-.937.915.915 1.063-1.063-.915-.915.937-.937.915.915 1.063-1.063-.915-.915Zm3.188-2.775c0-.935-1.131-1.404-1.793-.742l-9.975 9.976c-.662.661-.193 1.792.742 1.792H13.75V2.724Z" fill="#424242"/></svg>

After

Width:  |  Height:  |  Size: 401 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" clip-rule="evenodd" fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M8.5 1.423a1 1 0 0 0-1 0L2.554 4.278a1 1 0 0 0-.5.866v5.712a1 1 0 0 0 .5.866L7.5 14.577a1 1 0 0 0 1 0l4.946-2.855a1 1 0 0 0 .5-.866V5.144a1 1 0 0 0-.5-.866L8.5 1.423zM3.554 6.207v4.36l3.696 2.134V8.425L3.554 6.207zM8.75 12.7l3.696-2.134v-4.36L8.75 8.425V12.7zm2.868-7.746L8 2.866 4.382 4.955 8 7.125l3.618-2.17z" />
</svg>

After

Width:  |  Height:  |  Size: 492 B

View File

@@ -0,0 +1,15 @@
<!-- @license lucide-static v0.575.0 - ISC -->
<svg
class="lucide lucide-play"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="#424242"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z" />
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4 3a1 1 0 0 0-1 1v3h6v6h3a1 1 0 0 0 1-1V4a1 1 0 0 0-1-1H4z" /><path d="M8 8H2v5a1 1 0 0 0 1 1h5V8z" />
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M1.25 4c0-.966.784-1.75 1.75-1.75a.75.75 0 0 1 0 1.5.25.25 0 0 0-.25.25v2.586c0 .464-.184.909-.513 1.237L2.061 8l.176.177c.329.328.513.773.513 1.237V12c0 .138.112.25.25.25a.75.75 0 0 1 0 1.5A1.75 1.75 0 0 1 1.25 12V9.414a.25.25 0 0 0-.073-.177L.47 8.53a.75.75 0 0 1 0-1.06l.707-.707a.25.25 0 0 0 .073-.177V4zM4.75 11a.75.75 0 0 1 .75-.75H8a.75.75 0 0 1 0 1.5H5.5a.75.75 0 0 1-.75-.75zM4.75 8a.75.75 0 0 1 .75-.75H11a.75.75 0 0 1 0 1.5H5.5A.75.75 0 0 1 4.75 8zM5.5 4.25a.75.75 0 0 0 0 1.5h4a.75.75 0 0 0 0-1.5h-4zM13 13.75A1.75 1.75 0 0 0 14.75 12V9.414a.25.25 0 0 1 .073-.177l.707-.707a.75.75 0 0 0 0-1.06l-.707-.707a.25.25 0 0 1-.073-.177V4A1.75 1.75 0 0 0 13 2.25a.75.75 0 0 0 0 1.5.25.25 0 0 1 .25.25v2.586c0 .464.184.909.513 1.237l.176.177-.176.177a1.75 1.75 0 0 0-.513 1.237V12a.25.25 0 0 1-.25.25.75.75 0 0 0 0 1.5z" />
</svg>

After

Width:  |  Height:  |  Size: 963 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 16 16" fill="#424242" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M12.667 1.25c1.15 0 2.083.933 2.083 2.083v9.334c0 1.15-.933 2.083-2.083 2.083H3.333a2.083 2.083 0 0 1-2.083-2.083V3.333c0-1.15.933-2.083 2.083-2.083h9.334zm-9.917 9.5v1.917c0 .322.261.583.583.583H7.25v-2.5h-4.5zm6 0v2.5h3.917a.583.583 0 0 0 .583-.583V10.75h-4.5zm-6-1.5h4.5v-2.5h-4.5v2.5zm6 0h4.5v-2.5h-4.5v2.5zm-5.417-6.5a.583.583 0 0 0-.583.583V5.25h4.5v-2.5H3.333zm5.417 2.5h4.5V3.333a.583.583 0 0 0-.583-.583H8.75v2.5z" />
</svg>

After

Width:  |  Height:  |  Size: 564 B

Some files were not shown because too many files have changed in this diff Show More