diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php
index ee31b216d4..e986594e78 100644
--- a/src/__celerity_resource_map__.php
+++ b/src/__celerity_resource_map__.php
@@ -2973,6 +2973,15 @@ celerity_register_resource_map(array(
),
'disk' => '/rsrc/css/layout/phabricator-header-view.css',
),
+ 'phabricator-hovercard-view-css' =>
+ array(
+ 'uri' => '/res/061e66df/rsrc/css/layout/phabricator-hovercard-view.css',
+ 'type' => 'css',
+ 'requires' =>
+ array(
+ ),
+ 'disk' => '/rsrc/css/layout/phabricator-hovercard-view.css',
+ ),
'phabricator-jump-nav' =>
array(
'uri' => '/res/745c0e89/rsrc/css/application/directory/phabricator-jump-nav.css',
diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index d7542b179c..f5d37cb1f5 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1001,6 +1001,8 @@ phutil_register_library_map(array(
'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php',
'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php',
'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php',
+ 'PhabricatorHovercardExample' => 'applications/uiexample/examples/PhabricatorHovercardExample.php',
+ 'PhabricatorHovercardView' => 'view/widget/hovercard/PhabricatorHovercardView.php',
'PhabricatorIRCBot' => 'infrastructure/daemon/bot/PhabricatorIRCBot.php',
'PhabricatorIRCProtocolAdapter' => 'infrastructure/daemon/bot/adapter/PhabricatorIRCProtocolAdapter.php',
'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/bot/handler/PhabricatorIRCProtocolHandler.php',
@@ -2677,6 +2679,8 @@ phutil_register_library_map(array(
'PhabricatorHeaderView' => 'AphrontView',
'PhabricatorHelpController' => 'PhabricatorController',
'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController',
+ 'PhabricatorHovercardExample' => 'PhabricatorUIExample',
+ 'PhabricatorHovercardView' => 'AphrontView',
'PhabricatorIRCBot' => 'PhabricatorDaemon',
'PhabricatorIRCProtocolAdapter' => 'PhabricatorBaseProtocolAdapter',
'PhabricatorIRCProtocolHandler' => 'PhabricatorBotHandler',
diff --git a/src/applications/uiexample/examples/PhabricatorHovercardExample.php b/src/applications/uiexample/examples/PhabricatorHovercardExample.php
new file mode 100644
index 0000000000..6ceb8083f7
--- /dev/null
+++ b/src/applications/uiexample/examples/PhabricatorHovercardExample.php
@@ -0,0 +1,77 @@
+PhabricatorHovercardView to render '.
+ 'hovercards. Aren\'t I genius?');
+ }
+
+ public function renderExample() {
+ $request = $this->getRequest();
+ $user = $request->getUser();
+
+ $elements = array();
+
+ $diff_handle = $this->createBasicDummyHandle(
+ "Introduce cooler Differential Revisions",
+ PhabricatorPHIDConstants::PHID_TYPE_DREV);
+
+ $panel = $this->createPanel("Differential Hovercard");
+ $panel->appendChild(id(new PhabricatorHovercardView())
+ ->setObjectHandle($diff_handle)
+ ->addField(pht('Author'), $user->getUsername())
+ ->addField(pht('Updated'), phabricator_datetime(time(), $user))
+ ->addAction(pht('Subscribe'), '/dev/random')
+ ->setUser($user));
+ $elements[] = $panel;
+
+ $task_handle = $this->createBasicDummyHandle(
+ "Improve Mobile Experience for Phabricator",
+ PhabricatorPHIDConstants::PHID_TYPE_TASK);
+
+ $tag = id(new PhabricatorTagView())
+ ->setType(PhabricatorTagView::TYPE_STATE)
+ ->setBackgroundColor(PhabricatorTagView::COLOR_BLACK)
+ ->setName('Abandoned (Really)');
+ $panel = $this->createPanel("Maniphest Hovercard");
+ $panel->appendChild(id(new PhabricatorHovercardView())
+ ->setObjectHandle($task_handle)
+ ->setUser($user)
+ ->addField(pht('Assigned to'), $user->getUsername())
+ ->addField(pht('Dependent Tasks'), 'T123, T124, T125')
+ ->addAction(pht('Subscribe'), '/dev/random')
+ ->addAction(pht('Create Subtask'), '/dev/urandom')
+ ->addTag($tag));
+ $elements[] = $panel;
+
+ $user_handle = $this->createBasicDummyHandle(
+ 'gwashington',
+ PhabricatorPHIDConstants::PHID_TYPE_USER,
+ 'George Washington');
+ $user_handle->setImageURI(
+ celerity_get_resource_uri('/rsrc/image/people/washington.png'));
+ $panel = $this->createPanel("Whatevery Hovercard");
+ $panel->appendChild(id(new PhabricatorHovercardView())
+ ->setObjectHandle($user_handle)
+ ->addField(pht('Status'), 'Available')
+ ->addField(pht('Member since'), '30. February 1750')
+ ->addAction(pht('Start a Conpherence'), '/dev/null')
+ ->setUser($user));
+ $elements[] = $panel;
+
+ return phutil_implode_html("", $elements);
+ }
+
+ private function createPanel($header) {
+ $panel = new AphrontPanelView();
+ $panel->setNoBackground();
+ $panel->setHeader($header);
+ return $panel;
+ }
+
+}
diff --git a/src/applications/uiexample/examples/PhabricatorUIExample.php b/src/applications/uiexample/examples/PhabricatorUIExample.php
index 6688e489b3..7889b1ed86 100644
--- a/src/applications/uiexample/examples/PhabricatorUIExample.php
+++ b/src/applications/uiexample/examples/PhabricatorUIExample.php
@@ -17,4 +17,30 @@ abstract class PhabricatorUIExample {
abstract public function getDescription();
abstract public function renderExample();
+ protected function createBasicDummyHandle($name, $type, $fullname = null,
+ $uri = null) {
+
+ $id = mt_rand(15, 9999);
+ $handle = new PhabricatorObjectHandle();
+ $handle->setAlternateID(mt_rand(15, 9999));
+ $handle->setName($name);
+ $handle->setType($type);
+ $handle->setPHID(PhabricatorPHID::generateNewPHID($type));
+
+ if ($fullname) {
+ $handle->setFullName($fullname);
+ } else {
+ $handle->setFullName(sprintf('%s%d: %s',
+ substr($type, 0, 1),
+ $id,
+ $name));
+ }
+
+ if ($uri) {
+ $handle->setURI($uri);
+ }
+
+ return $handle;
+ }
+
}
diff --git a/src/view/widget/hovercard/PhabricatorHovercardView.php b/src/view/widget/hovercard/PhabricatorHovercardView.php
new file mode 100644
index 0000000000..86b482fd43
--- /dev/null
+++ b/src/view/widget/hovercard/PhabricatorHovercardView.php
@@ -0,0 +1,170 @@
+handle = $handle;
+ return $this;
+ }
+
+ public function setTitle($title) {
+ $this->title = $title;
+ return $this;
+ }
+
+ public function setDetail($detail) {
+ $this->detail = $detail;
+ return $this;
+ }
+
+ public function addField($label, $value) {
+ $this->fields[] = array(
+ 'label' => $label,
+ 'value' => $value,
+ );
+ return $this;
+ }
+
+ public function addAction($label, $uri, $workflow = false) {
+ $this->actions[] = array(
+ 'label' => $label,
+ 'uri' => $uri,
+ 'workflow' => $workflow,
+ );
+ return $this;
+ }
+
+ public function addTag(PhabricatorTagView $tag) {
+ $this->tags[] = $tag;
+ return $this;
+ }
+
+ public function setColor($color) {
+ $this->color = $color;
+ return $this;
+ }
+
+ public function render() {
+ $handle = $this->handle;
+ $user = $this->getUser();
+
+ $id = $handle->getAlternateID();
+ $type = $handle->getType();
+
+ require_celerity_resource("phabricator-hovercard-view-css");
+
+ $title = array();
+ if ($this->tags) {
+ $title[] = ' ';
+ $title[] = phutil_tag(
+ 'span',
+ array(
+ 'class' => 'phabricator-hovercard-tags',
+ ),
+ array_interleave(' ', $this->tags));
+ }
+ $title[] = pht("%s: %s", $handle->getTypeName(), substr($type, 0, 1) . $id);
+
+ $body = array();
+ if ($this->detail) {
+ $body[] = hsprintf('%s', $this->detail);
+ } else {
+ // Fallback for object handles
+ $body[] = hsprintf('%s', $handle->getFullName());
+ }
+
+ foreach ($this->fields as $field) {
+ $body[] = hsprintf('%s: %s',
+ $field['label'], $field['value']);
+ }
+
+ $body = phutil_implode_html(phutil_tag('br'), $body);
+
+ if ($handle->getImageURI()) {
+ // Probably a user, we don't need to assume something else
+ // "Prepend" the image by appending $body
+ $body = phutil_tag(
+ 'div',
+ array(
+ 'class' => 'profile-header-picture-frame',
+ 'style' => 'background-image: url('.$handle->getImageURI().');',
+ ),
+ '')
+ ->appendHTML($body);
+ }
+
+ $buttons = array();
+
+ foreach ($this->actions as $action) {
+ $options = array(
+ 'class' => 'button grey',
+ 'href' => $action['uri'],
+ );
+
+ if ($action['workflow']) {
+ $options['sigil'] = 'workflow';
+ $buttons[] = javelin_tag(
+ 'a',
+ $options,
+ $action['label']);
+ } else {
+ $buttons[] = phutil_tag(
+ 'a',
+ $options,
+ $action['label']);
+ }
+ }
+
+ $tail = null;
+ if ($buttons) {
+ $tail = phutil_tag('div',
+ array('class' => 'phabricator-hovercard-tail'),
+ $buttons);
+ }
+
+ // Assemble container
+ // TODO: Add color support
+ $content = hsprintf(
+ '%s%s%s',
+ phutil_tag('div', array('class' => 'phabricator-hovercard-head'), $title),
+ phutil_tag('div', array('class' => 'phabricator-hovercard-body'), $body),
+ $tail);
+
+ $hovercard = phutil_tag("div",
+ array(
+ "class" => "phabricator-hovercard-container",
+ ),
+ $content);
+
+ // Wrap for thick border
+ // and later the tip at the bottom
+ return phutil_tag('div',
+ array(
+ 'class' => 'phabricator-hovercard-wrapper',
+ ),
+ $hovercard);
+ }
+
+}
diff --git a/webroot/rsrc/css/layout/phabricator-hovercard-view.css b/webroot/rsrc/css/layout/phabricator-hovercard-view.css
new file mode 100644
index 0000000000..2aeb6499a3
--- /dev/null
+++ b/webroot/rsrc/css/layout/phabricator-hovercard-view.css
@@ -0,0 +1,69 @@
+/**
+ * @provides phabricator-hovercard-view-css
+ */
+
+.phabricator-hovercard-wrapper {
+ border-radius: 4px;
+ width: 400px;
+ margin: auto;
+ padding: 4px;
+ background-color: #cccccc;
+}
+
+.device-phone .phabricator-hovercard-wrapper {
+ width: 300px;
+}
+
+.phabricator-hovercard-container {
+ border-radius: 3px;
+ border: 1px solid #666666;
+}
+
+.phabricator-hovercard-head {
+ padding: 10px 15px;
+ font-weight: bold;
+ font-size: 15px;
+ white-space: nowrap;
+ color: white;
+ text-shadow: 0 1px 0 #333333;
+ font-weight: bold;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ overflow: hidden;
+
+ background-color: #559911;
+}
+
+.phabricator-hovercard-tags {
+ font-size: 13px;
+ float: right;
+ white-space: normal;
+}
+
+.phabricator-hovercard-body {
+ padding: 15px;
+ background-color: white;
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+}
+
+.phabricator-hovercard-body .profile-header-picture-frame {
+ float: left;
+ margin: 0;
+ margin-right: 10px;
+ margin-bottom: 5px;
+ width: 50px;
+ height: 50px;
+}
+
+.phabricator-hovercard-tail {
+ padding: 3px 2px;
+ background-color: #eeeeee;
+ border-bottom-left-radius: 2px;
+ border-bottom-right-radius: 2px;
+}
+
+.phabricator-hovercard-tail button,
+.phabricator-hovercard-tail a.button {
+ margin: 3px;
+}