diff --git a/.github/workflows/version-sync.yml b/.github/workflows/version-sync.yml
index 258ab7e6..bd496080 100644
--- a/.github/workflows/version-sync.yml
+++ b/.github/workflows/version-sync.yml
@@ -3,13 +3,15 @@ name: Sync version artifacts
# Triggered whenever a GitHub release or pre-release is published.
# Checks out main, aligns all non-JS version references to the release tag,
# rebuilds CSS bundles, publishes them to the dist branch, captures the
-# resulting commit SHA, stores it in SLASHED_BRICKS_DIST_SHA, and commits
+# resulting commit SHA, stores it in SLASHED_DIST_SHA (slashed.php),
+# SLASHED_BRICKS_DIST_SHA (slashed-bricks.php), and
+# SLASHED_GUTENBERG_DIST_SHA (slashed-gutenberg.php), then commits
# everything back to main.
#
# CSS bundles are NOT committed to the main branch — they live only on the
# dedicated `dist` branch and in GitHub Release assets. The dist-branch
-# commit SHA is stored in slashed-bricks.php so the plugin can construct an
-# immutable jsDelivr CDN URL that never changes between releases.
+# commit SHA is stored in each integration entry point so the plugin can
+# construct an immutable jsDelivr CDN URL that never changes between releases.
#
# Full release pipeline:
# 1. Developer runs `npm run release` locally → bumps package.json, runs
@@ -106,12 +108,19 @@ jobs:
- name: Switch back to main working tree
run: git checkout main
- - name: Update SLASHED_BRICKS_DIST_SHA in PHP
+ - name: Update DIST_SHA constants in PHP
+ env:
+ DIST_SHA: ${{ steps.dist.outputs.sha }}
run: |
- DIST_SHA="${{ steps.dist.outputs.sha }}"
+ sed -i \
+ "s|define( 'SLASHED_DIST_SHA', '[^']*' )|define( 'SLASHED_DIST_SHA', '${DIST_SHA}' )|" \
+ slashed.php
sed -i \
"s|define( 'SLASHED_BRICKS_DIST_SHA', '[^']*' )|define( 'SLASHED_BRICKS_DIST_SHA', '${DIST_SHA}' )|" \
integrations/bricks/slashed-bricks.php
+ sed -i \
+ "s|define( 'SLASHED_GUTENBERG_DIST_SHA', '[^']*' )|define( 'SLASHED_GUTENBERG_DIST_SHA', '${DIST_SHA}' )|" \
+ integrations/gutenberg/slashed-gutenberg.php
- name: Commit and push if anything changed
env:
@@ -120,7 +129,7 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# Stage only tracked files (dist/*.css are gitignored).
- git add integrations/bricks/slashed-bricks.php package.json package-lock.json
+ git add slashed.php integrations/bricks/slashed-bricks.php integrations/gutenberg/slashed-gutenberg.php package.json package-lock.json
git add -u
if git diff --cached --quiet; then
echo "Nothing to commit — all version artifacts already up to date."
diff --git a/.gitignore b/.gitignore
index 3f324d91..c3721f8c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,6 @@ semantic-review/
# Build artifacts in dist/ — generated by `npm run build`, served via the
# `dist` branch (version-sync.yml) and GitHub Releases. Not tracked in main
# to avoid binary conflicts and noise in PR diffs.
-dist/slashed-bricks.zip
+dist/slashed.zip
dist/*.css
dist/*.map
diff --git a/docs/roadmap.md b/docs/roadmap.md
index aa648ce1..0873edf1 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -110,12 +110,20 @@ stable and locked.
Ideas that are promising but not yet committed.
-- **Gutenberg integration** — CSS loading + color palette sync + `theme.json`
- custom property mapping. Scoped v1 is ~2–3 weeks of work and covers 80% of
- what Gutenberg users need. Deferred because the `--sf-*` → `--wp--custom--*`
- naming mismatch needs a clean translation layer, and Gutenberg's per-block
- architecture is fundamentally different from Bricks' canvas model. reBEMer
- parity is not a goal for any Gutenberg integration.
+- **Gutenberg integration** *(in progress — `integrations/gutenberg/`)* — v1
+ ships as a standalone plugin (`slashed-gutenberg.php`) that can be activated
+ independently of the Bricks plugin. v1 scope: CSS bundle loading into the
+ block editor canvas + frontend, color palette sync (21 tokens — brand,
+ status, surface, text, border, link), and a `data-wp-dark-mode-active` bridge
+ so the editor dark-mode toggle drives SLASHED's color-scheme system.
+
+ Deliberately excluded from v1 (future work):
+ - `--sf-*` → `--wp--custom--*` theme.json mapping — only needed to expose
+ tokens in the Global Styles UI (Site Editor); not required for the 80% use
+ case. Analogy: ACSS does not do this either.
+ - Token override admin UI — no Svelte panel for Gutenberg yet; overrides via
+ the `slashed_gutenberg/css_bundle_url` filter or child-theme CSS.
+ - reBEMer parity — Bricks-specific; not applicable to the block editor.
- **Per-layer opt-in bundle via `@import`** — technically already possible
today using the granular `core/` and `optional/` source files (with
diff --git a/includes/class-admin.php b/includes/class-admin.php
new file mode 100644
index 00000000..ef89c8f9
--- /dev/null
+++ b/includes/class-admin.php
@@ -0,0 +1,183 @@
+ in_array( $raw_bundle, Slashed_Settings::ALLOWED_BUNDLES, true ) ? $raw_bundle : 'optimal',
+ 'integrations' => array(),
+ );
+ foreach ( Slashed_Settings::KNOWN_INTEGRATIONS as $slug ) {
+ $data['integrations'][ $slug ] = array_key_exists( $slug, $raw_integrations );
+ }
+
+ Slashed_Settings::save( $data );
+
+ wp_safe_redirect(
+ add_query_arg( 'slashed_saved', '1', admin_url( 'admin.php?page=' . self::PAGE_SLUG ) )
+ );
+ exit;
+ }
+
+ public function render_page() {
+ if ( ! current_user_can( 'manage_options' ) ) {
+ return;
+ }
+
+ $settings = Slashed_Settings::get();
+ $integrations = $settings['integrations'];
+ $css_bundle = $settings['css_bundle'];
+ $saved = ! empty( $_GET['slashed_saved'] ); // phpcs:ignore WordPress.Security.NonceVerification
+ ?>
+
+
+
+
+
+
+
+
+
+
+
+ ' . esc_html( substr( SLASHED_DIST_SHA, 0, 8 ) ) . ''
+ );
+ ?>
+
+
+ , css_bundle: string}
+ */
+ public static function get() {
+ $stored = get_option( self::OPTION_KEY, array() );
+ if ( ! is_array( $stored ) ) {
+ $stored = array();
+ }
+
+ return array(
+ 'integrations' => self::get_integrations( $stored ),
+ 'css_bundle' => self::get_css_bundle( $stored ),
+ );
+ }
+
+ /**
+ * Get the configured CSS bundle variant.
+ *
+ * Reads from shared settings; defaults to 'optimal'.
+ * Called directly by Slashed_CSS_Loader so integrations do not need to
+ * implement bundle resolution themselves.
+ *
+ * @param array|null $stored Pre-fetched stored option (avoids double DB read).
+ * @return string
+ */
+ public static function get_css_bundle( $stored = null ) {
+ if ( null === $stored ) {
+ $stored = get_option( self::OPTION_KEY, array() );
+ if ( ! is_array( $stored ) ) {
+ $stored = array();
+ }
+ }
+ $bundle = isset( $stored['css_bundle'] ) ? (string) $stored['css_bundle'] : 'optimal';
+ return in_array( $bundle, self::ALLOWED_BUNDLES, true ) ? $bundle : 'optimal';
+ }
+
+ /**
+ * Whether a given integration is enabled.
+ *
+ * Defaults to true so a fresh install activates every integration;
+ * users explicitly disable what they do not need.
+ *
+ * @param string $integration Integration slug (e.g. 'bricks', 'gutenberg').
+ * @return bool
+ */
+ public static function is_enabled( $integration ) {
+ $settings = self::get();
+ $flags = $settings['integrations'];
+ // Unknown integrations default to false (future-compat: a new integration
+ // added in a later release should not silently activate on upgrade).
+ if ( ! in_array( $integration, self::KNOWN_INTEGRATIONS, true ) ) {
+ return false;
+ }
+ return isset( $flags[ $integration ] ) ? (bool) $flags[ $integration ] : true;
+ }
+
+ /**
+ * Persist new settings values.
+ *
+ * Only known, sanitized keys are written. Unrecognised keys are silently
+ * dropped to prevent arbitrary data from landing in the option.
+ *
+ * @param array $data Raw submitted data (e.g. from $_POST).
+ * @return bool True if the value was updated, false if unchanged or invalid.
+ */
+ public static function save( array $data ) {
+ $integrations = array();
+ foreach ( self::KNOWN_INTEGRATIONS as $slug ) {
+ $integrations[ $slug ] = ! empty( $data['integrations'][ $slug ] );
+ }
+
+ $bundle = isset( $data['css_bundle'] ) ? (string) $data['css_bundle'] : 'optimal';
+ if ( ! in_array( $bundle, self::ALLOWED_BUNDLES, true ) ) {
+ $bundle = 'optimal';
+ }
+
+ return update_option( self::OPTION_KEY, array(
+ 'integrations' => $integrations,
+ 'css_bundle' => $bundle,
+ ) );
+ }
+
+ /**
+ * @param array $stored Raw stored option value.
+ * @return array
+ */
+ private static function get_integrations( array $stored ) {
+ $flags = isset( $stored['integrations'] ) && is_array( $stored['integrations'] )
+ ? $stored['integrations']
+ : array();
+
+ $result = array();
+ foreach ( self::KNOWN_INTEGRATIONS as $slug ) {
+ // Default true: new installs run all integrations until turned off.
+ $result[ $slug ] = isset( $flags[ $slug ] ) ? (bool) $flags[ $slug ] : true;
+ }
+ return $result;
+ }
+}
diff --git a/integrations/bricks/includes/class-admin-page-svelte.php b/integrations/bricks/includes/class-admin-page-svelte.php
index 2c701e00..4274db20 100644
--- a/integrations/bricks/includes/class-admin-page-svelte.php
+++ b/integrations/bricks/includes/class-admin-page-svelte.php
@@ -54,18 +54,34 @@ public function __construct() {
}
/**
- * Register the top-level SLASHED admin menu.
+ * Register the SLASHED admin menu.
+ *
+ * Standalone plugin: registers a top-level "SLASHED" menu.
+ * Unified plugin (SLASHED_VERSION defined): registers as a sub-page
+ * under the top-level "SLASHED" menu owned by slashed.php so only one
+ * SLASHED item appears in the sidebar.
*/
public function register_menu() {
- $this->hook_suffix = (string) add_menu_page(
- __( 'SLASHED Settings', 'slashed-bricks' ),
- __( 'SLASHED', 'slashed-bricks' ),
- 'manage_options',
- self::PAGE_SLUG,
- array( $this, 'render_page' ),
- 'dashicons-art',
- 59
- );
+ if ( defined( 'SLASHED_VERSION' ) ) {
+ $this->hook_suffix = (string) add_submenu_page(
+ 'slashed',
+ __( 'Bricks Tokens', 'slashed-bricks' ),
+ __( 'Bricks Tokens', 'slashed-bricks' ),
+ 'manage_options',
+ self::PAGE_SLUG,
+ array( $this, 'render_page' )
+ );
+ } else {
+ $this->hook_suffix = (string) add_menu_page(
+ __( 'SLASHED Settings', 'slashed-bricks' ),
+ __( 'SLASHED', 'slashed-bricks' ),
+ 'manage_options',
+ self::PAGE_SLUG,
+ array( $this, 'render_page' ),
+ 'dashicons-art',
+ 59
+ );
+ }
}
/**
diff --git a/integrations/bricks/includes/class-enqueue.php b/integrations/bricks/includes/class-enqueue.php
index 649c81be..9ecc1a0c 100644
--- a/integrations/bricks/includes/class-enqueue.php
+++ b/integrations/bricks/includes/class-enqueue.php
@@ -61,20 +61,17 @@ public function enqueue_frontend_styles() {
return;
}
- // Use file modification time for cache-busting when the CSS exists locally.
- // Derive the filename from the already-resolved URL so we don't re-read
- // plugin settings a second time (slashed_bricks_get_css_url() already did).
- $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) );
-
- if ( '' === $filename ) {
- $version = SLASHED_BRICKS_VERSION;
+ // Cache-busting: mtime for local files, version constant for CDN.
+ if ( class_exists( 'Slashed_CSS_Loader' ) ) {
+ $version = Slashed_CSS_Loader::get_version( $css_url );
} else {
+ $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) );
$repo_path = SLASHED_BRICKS_PATH . '../../dist/' . $filename;
$local_path = SLASHED_BRICKS_PATH . 'dist/' . $filename;
- if ( file_exists( $repo_path ) ) {
+ if ( '' !== $filename && file_exists( $repo_path ) ) {
$version = (string) filemtime( $repo_path );
- } elseif ( file_exists( $local_path ) ) {
+ } elseif ( '' !== $filename && file_exists( $local_path ) ) {
$version = (string) filemtime( $local_path );
} else {
$version = SLASHED_BRICKS_VERSION;
diff --git a/integrations/bricks/slashed-bricks.php b/integrations/bricks/slashed-bricks.php
index 0df90311..3b906811 100644
--- a/integrations/bricks/slashed-bricks.php
+++ b/integrations/bricks/slashed-bricks.php
@@ -18,44 +18,39 @@
/**
* Plugin constants.
- */
-define( 'SLASHED_BRICKS_VERSION', '0.4.18' );
-define( 'SLASHED_BRICKS_PATH', plugin_dir_path( __FILE__ ) );
-define( 'SLASHED_BRICKS_URL', plugin_dir_url( __FILE__ ) );
-
-/**
- * Immutable jsDelivr ref for the SLASHED CSS bundle.
*
- * Points to the HEAD commit SHA of the `dist` branch at the time of the
- * last release. jsDelivr treats commit SHAs as effectively immutable
- * (cached "forever"), so the served CSS cannot change outside a plugin
- * release even though the `dist` branch itself is a moving ref.
- *
- * CSS bundles are no longer committed to the main branch; they live only
- * on the `dist` branch (files at root) and in GitHub Release assets.
- * Updated automatically by the version-sync workflow on every release.
- *
- * Override per-site with the 'slashed_bricks/css_bundle_url' filter.
+ * Defined via plugin_dir_path(__FILE__) so paths are correct whether this
+ * file is loaded standalone or included by the unified slashed.php. The
+ * !defined() guard exists solely to prevent redefinition errors when both
+ * the standalone Bricks plugin and the unified SLASHED plugin are active.
*/
-define( 'SLASHED_BRICKS_CSS_REF', 'v0.4.18' ); // semver tag — version comparison only
-define( 'SLASHED_BRICKS_DIST_SHA', 'be9ac0789180158c8ad86d5743020ef2272a063c' ); // dist branch SHA — CDN URL
+if ( ! defined( 'SLASHED_BRICKS_VERSION' ) ) {
+ define( 'SLASHED_BRICKS_VERSION', '0.4.18' );
+ define( 'SLASHED_BRICKS_PATH', plugin_dir_path( __FILE__ ) );
+ define( 'SLASHED_BRICKS_URL', plugin_dir_url( __FILE__ ) );
+ define( 'SLASHED_BRICKS_CSS_REF', 'v0.4.18' );
+ define( 'SLASHED_BRICKS_DIST_SHA', 'be9ac0789180158c8ad86d5743020ef2272a063c' );
+}
/**
* Token Store is loaded early so slashed_bricks_get_css_bundle() can call
* Slashed_Bricks_Token_Store::get_plugin_settings() at any point, even
* before the rest of the admin/data classes are initialised.
*/
-require_once plugin_dir_path( __FILE__ ) . 'includes/class-token-store.php';
+require_once SLASHED_BRICKS_PATH . 'includes/class-token-store.php';
/**
- * Get the configured CSS bundle type (essential / optimal / full).
+ * Get the configured CSS bundle variant.
*
- * Reads from plugin settings; falls back to "optimal". Used both for
- * URL resolution and for local-file version-stamp lookups.
+ * Delegates to the shared Slashed_CSS_Loader when running under the unified
+ * plugin; falls back to the Bricks token store in standalone mode.
*
* @return string One of 'essential', 'optimal', 'full'.
*/
function slashed_bricks_get_css_bundle() {
+ if ( class_exists( 'Slashed_CSS_Loader' ) ) {
+ return Slashed_CSS_Loader::get_bundle();
+ }
$settings = Slashed_Bricks_Token_Store::get_plugin_settings();
$bundle = isset( $settings['css_bundle'] ) ? (string) $settings['css_bundle'] : 'optimal';
if ( ! in_array( $bundle, Slashed_Bricks_Token_Store::ALLOWED_CSS_BUNDLES, true ) ) {
@@ -67,44 +62,35 @@ function slashed_bricks_get_css_bundle() {
/**
* Get the URL for the SLASHED CSS bundle.
*
- * Defaults to the jsDelivr CDN pinned to the immutable dist-branch commit
- * SHA (see SLASHED_BRICKS_DIST_SHA) so the plugin works without any local
- * file setup. The specific file (essential / optimal / full) is chosen
- * from the 'css_bundle' plugin setting. If a local copy is detected
- * (symlink/in-repo mode or copy-install mode), the local file takes
- * precedence for faster loads and offline development.
+ * Delegates to the shared Slashed_CSS_Loader when running under the unified
+ * plugin, then applies the per-integration filter. In standalone mode, builds
+ * the URL directly from SLASHED_BRICKS_DIST_SHA with a local-file fallback.
*
* Use the 'slashed_bricks/css_bundle_url' filter to override.
*
* @return string URL to the CSS bundle.
*/
function slashed_bricks_get_css_url() {
- $bundle = slashed_bricks_get_css_bundle();
- $filename = 'slashed.' . $bundle . '.css';
+ if ( class_exists( 'Slashed_CSS_Loader' ) ) {
+ return apply_filters( 'slashed_bricks/css_bundle_url', Slashed_CSS_Loader::get_url() );
+ }
- // Default: jsDelivr CDN pinned to the dist-branch commit SHA at release
- // time. Files live at the root of the dist branch (no /dist/ prefix).
+ // Standalone fallback.
+ $bundle = slashed_bricks_get_css_bundle();
+ $filename = 'slashed.' . $bundle . '.css';
$default_url = sprintf(
'https://cdn.jsdelivr.net/gh/codeslash-dev/SLASHED@%s/%s',
SLASHED_BRICKS_DIST_SHA,
$filename
);
- // Prefer local file if available (symlink/in-repo mode).
$repo_path = SLASHED_BRICKS_PATH . '../../dist/' . $filename;
if ( file_exists( $repo_path ) ) {
$default_url = SLASHED_BRICKS_URL . '../../dist/' . $filename;
- }
- // Check copy-install mode (dist/ within the plugin directory).
- elseif ( file_exists( SLASHED_BRICKS_PATH . 'dist/' . $filename ) ) {
+ } elseif ( file_exists( SLASHED_BRICKS_PATH . 'dist/' . $filename ) ) {
$default_url = SLASHED_BRICKS_URL . 'dist/' . $filename;
}
- /**
- * Filter the SLASHED CSS bundle URL.
- *
- * @param string $url The URL to the CSS bundle file.
- */
return apply_filters( 'slashed_bricks/css_bundle_url', $default_url );
}
@@ -270,7 +256,11 @@ function slashed_bricks_activation_check() {
// Allow activation without Bricks so admin token configuration is available.
// Runtime guards in slashed_bricks_init() handle the Bricks dependency.
}
-register_activation_hook( __FILE__, 'slashed_bricks_activation_check' );
+// Only register hooks against the standalone plugin file; when running under
+// the unified slashed.php the root plugin handles activation/deactivation.
+if ( ! defined( 'SLASHED_VERSION' ) ) {
+ register_activation_hook( __FILE__, 'slashed_bricks_activation_check' );
+}
/**
* Display admin notice when Bricks Builder is not active.
@@ -376,7 +366,9 @@ function slashed_bricks_deactivation_cleanup() {
wp_unschedule_event( $timestamp, 'slashed_bricks_version_check' );
}
}
-register_deactivation_hook( __FILE__, 'slashed_bricks_deactivation_cleanup' );
+if ( ! defined( 'SLASHED_VERSION' ) ) {
+ register_deactivation_hook( __FILE__, 'slashed_bricks_deactivation_cleanup' );
+}
/**
* Dashboard widget content: informs the user a newer framework version exists.
diff --git a/integrations/gutenberg/includes/class-color-palette.php b/integrations/gutenberg/includes/class-color-palette.php
new file mode 100644
index 00000000..1390bc82
--- /dev/null
+++ b/integrations/gutenberg/includes/class-color-palette.php
@@ -0,0 +1,134 @@
+build_palette() );
+ // Block/FSE themes: theme.json takes precedence; inject via the data filter.
+ add_filter( 'wp_theme_json_data_theme', array( $this, 'inject_theme_json_palette' ) );
+ }
+
+ /**
+ * Inject the SLASHED palette into the theme.json data for block/FSE themes.
+ *
+ * Uses WP_Theme_JSON_Data::update_with() which merges presets by slug, so
+ * the theme's own palette entries are preserved alongside ours. All SLASHED
+ * slugs are prefixed with 'slashed-' to avoid collisions.
+ *
+ * @param WP_Theme_JSON_Data $theme_json Theme JSON data object.
+ * @return WP_Theme_JSON_Data
+ */
+ public function inject_theme_json_palette( $theme_json ) {
+ $theme_json->update_with( array(
+ 'version' => 3,
+ 'settings' => array(
+ 'color' => array(
+ 'palette' => $this->build_palette(),
+ ),
+ ),
+ ) );
+ return $theme_json;
+ }
+
+ /**
+ * Build the full palette array from the token definitions.
+ *
+ * @return array[]
+ */
+ private function build_palette() {
+ $entries = array();
+
+ foreach ( $this->get_definitions() as $def ) {
+ $entries[] = array(
+ 'name' => $def['name'],
+ 'slug' => 'slashed-' . $def['slug'],
+ 'color' => 'var(--sf-color-' . $def['token'] . ')',
+ );
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Token definitions: slug (WP class-name fragment), label, CSS token suffix.
+ *
+ * `slug` — appended to 'slashed-' for the WP color slug (single dashes only).
+ * `token` — the `--sf-color-{token}` custom property suffix (may contain --).
+ * `name` — human-readable label shown in the color picker.
+ *
+ * @return array[]
+ */
+ private function get_definitions() {
+ return array(
+ // Brand
+ array( 'slug' => 'primary', 'token' => 'primary', 'name' => __( 'Primary', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'secondary', 'token' => 'secondary', 'name' => __( 'Secondary', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'tertiary', 'token' => 'tertiary', 'name' => __( 'Tertiary', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'action', 'token' => 'action', 'name' => __( 'Action', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'neutral', 'token' => 'neutral', 'name' => __( 'Neutral', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'base', 'token' => 'base', 'name' => __( 'Base', 'slashed-gutenberg' ) ),
+ // Status
+ array( 'slug' => 'success', 'token' => 'success', 'name' => __( 'Success', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'warning', 'token' => 'warning', 'name' => __( 'Warning', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'error', 'token' => 'error', 'name' => __( 'Error', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'info', 'token' => 'info', 'name' => __( 'Info', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'danger', 'token' => 'danger', 'name' => __( 'Danger', 'slashed-gutenberg' ) ),
+ // Surface
+ array( 'slug' => 'bg', 'token' => 'bg', 'name' => __( 'Background', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'surface', 'token' => 'surface', 'name' => __( 'Surface', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'well', 'token' => 'well', 'name' => __( 'Well', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'raised', 'token' => 'raised', 'name' => __( 'Raised', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'inverse', 'token' => 'inverse', 'name' => __( 'Inverse', 'slashed-gutenberg' ) ),
+ // Text
+ array( 'slug' => 'text', 'token' => 'text', 'name' => __( 'Text', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'text-secondary', 'token' => 'text--secondary', 'name' => __( 'Text Secondary', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'text-muted', 'token' => 'text--muted', 'name' => __( 'Text Muted', 'slashed-gutenberg' ) ),
+ // Border & link
+ array( 'slug' => 'border', 'token' => 'border', 'name' => __( 'Border', 'slashed-gutenberg' ) ),
+ array( 'slug' => 'link', 'token' => 'link', 'name' => __( 'Link', 'slashed-gutenberg' ) ),
+ );
+ }
+}
diff --git a/integrations/gutenberg/includes/class-enqueue.php b/integrations/gutenberg/includes/class-enqueue.php
new file mode 100644
index 00000000..c0003103
--- /dev/null
+++ b/integrations/gutenberg/includes/class-enqueue.php
@@ -0,0 +1,110 @@
+ (WP 6.4+). An inline rule maps that attribute into SLASHED's
+ * theme layer so the editing surface tracks the editor's dark-mode toggle.
+ * The rule is harmless on the frontend where the attribute is never set.
+ */
+class Slashed_Gutenberg_Enqueue {
+
+ public function __construct() {
+ add_action( 'enqueue_block_editor_assets', array( $this, 'enqueue_editor_styles' ) );
+ add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_styles' ) );
+ }
+
+ /**
+ * Load SLASHED CSS into the block editor.
+ *
+ * `enqueue_block_editor_assets` fires for both the post editor and the
+ * site editor (FSE). Styles registered here are injected directly into
+ * the editor iframe so they affect the editing canvas, not the
+ * surrounding wp-admin chrome.
+ */
+ public function enqueue_editor_styles() {
+ $css_url = slashed_gutenberg_get_css_url();
+ if ( '' === $css_url ) {
+ return;
+ }
+
+ wp_enqueue_style(
+ 'slashed-framework',
+ $css_url,
+ array(),
+ $this->get_version( $css_url )
+ );
+
+ // Bridge the Gutenberg dark-mode toggle to SLASHED's theme system.
+ wp_add_inline_style(
+ 'slashed-framework',
+ '@layer slashed.themes{html[data-wp-dark-mode-active]{color-scheme:dark;--sf-is-dark:1}}'
+ );
+ }
+
+ /**
+ * Load SLASHED CSS on the public frontend.
+ */
+ public function enqueue_frontend_styles() {
+ $css_url = slashed_gutenberg_get_css_url();
+ if ( '' === $css_url ) {
+ return;
+ }
+
+ wp_enqueue_style(
+ 'slashed-framework',
+ $css_url,
+ array(),
+ $this->get_version( $css_url )
+ );
+ }
+
+ /**
+ * Derive a cache-busting version string from the local file mtime when
+ * available; falls back to the plugin version constant.
+ *
+ * @param string $css_url Resolved URL for the bundle.
+ * @return string
+ */
+ private function get_version( $css_url ) {
+ if ( class_exists( 'Slashed_CSS_Loader' ) ) {
+ return Slashed_CSS_Loader::get_version( $css_url );
+ }
+
+ // Standalone fallback.
+ $filename = basename( (string) wp_parse_url( $css_url, PHP_URL_PATH ) );
+ if ( '' === $filename ) {
+ return SLASHED_GUTENBERG_VERSION;
+ }
+
+ $repo_path = SLASHED_GUTENBERG_PATH . '../../dist/' . $filename;
+ $local_path = SLASHED_GUTENBERG_PATH . 'dist/' . $filename;
+
+ if ( file_exists( $repo_path ) ) {
+ return (string) filemtime( $repo_path );
+ }
+ if ( file_exists( $local_path ) ) {
+ return (string) filemtime( $local_path );
+ }
+
+ return SLASHED_GUTENBERG_VERSION;
+ }
+}
diff --git a/integrations/gutenberg/slashed-gutenberg.php b/integrations/gutenberg/slashed-gutenberg.php
new file mode 100644
index 00000000..32be0629
--- /dev/null
+++ b/integrations/gutenberg/slashed-gutenberg.php
@@ -0,0 +1,106 @@
+>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
- }
+ for (let j = 0; j < 8; j++) crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
return (crc ^ 0xffffffff) >>> 0;
}
diff --git a/slashed.php b/slashed.php
new file mode 100644
index 00000000..88473e5b
--- /dev/null
+++ b/slashed.php
@@ -0,0 +1,81 @@
+